Ad-hoc polymorphism erodes type-safety

In my years of experience, I developed a rule of thumb of when an abstraction erodes type-safety.

I call typeclasses like Foldable structure-oblivious because they don’t preserve the structure of the argument in their result. That’s why they’re potentially dangerous because if you add more layers on top of your argument you have no way to leverage the type checker to catch potentially wrong behaviour earlier. So you must rely on other tools to verify your code (tests, LiquidHaskell, manual code review, etc.).

Examples of structure-oblivious abstractions:

length :: Foldable f => f a -> Int
show :: Show a => a -> String
toJSON :: ToJSON a => a -> Value

In contrast, some typeclasses are structure-preserving. The type of the structure is preserved in the result. That’s why all types propagate. If you change a value from settingAllowList :: [ClientIdentifier] to settingAllowList :: Maybe [ClientIdentifier], functions like fmap fromClientIdentifier settingAllowList don’t compile anymore.

Examples of structure-preserving abstractions:

(<>) :: Semigroup a => a -> a -> a
fmap :: Functor f => (a -> b) -> f a -> f b

I noticed, when there’s a bug due to a usage of a typeclass, it’s usually because the typeclass is structure-oblivious.

11 Likes