2
votes

To clarify my question, let me rephrase it in a more or less equivalent way:

Why is there a concept of superclass/class inheritance in Haskell? What are the historical reasons that led to that design choice? Why would it be so bad, for example, to have a base library with no class hierarchy, just typeclasses independent from each other?

Here I'll expose some random thoughts that made me want to ask this question. My current intuitions might be inaccurate as they are based on my current understanding of Haskell which is not perfect, but here they are...

It is not obvious to me why type class inheritance exists in Haskell. I find it a bit weird, as it creates asymmetry in concepts. Often in mathematics, concepts can be defined from different viewpoints, I don't necessarily want to favor an order of how they ought to be defined. OK there is some order in which one should prove things, but once theorems and structures are there, I'd rather see them as available independent tools.

Moreover one perhaps not so good thing I see with class inheritance is this: I think a class instance will silently pick a corresponding superclass instance, which was probably implemented to be the most natural one for that type. Let's consider a Monad viewed as a subclass of Functor. Maybe there could be more than one way to define a Functor on some type that also happens to be a Monad. But saying that a Monad is a Functor implicitly makes the choice of one particular Functor for that Monad. Someday, you might forget that actually you wanted some other Functor. Perhaps this example is not the best fit, but I have the feeling this sort of situation might generalize and possibly be dangerous if your class is a child of many. Current Haskell inheritance sounds like it makes default choices about parents implicitly.

If instead you have a design without hierarchy, I feel you would always have to be explicit about all the properties required, which would perhaps mean a bit less risk, more clarity, and more symmetry. So far, what I'm seeing is that the cost of such a design, would be : more constraints to write in instance definitions, and newtype wrappers, for each meaningful conversion from one set of concepts to another. I am not sure, but perhaps that could have been acceptable. Unfortunately, I think Haskell auto deriving mechanism for newtypes doesn't work very well, I would appreciate that the language was somehow smarter with newtype wrapping/unwrapping and required less verbosity. I'm not sure, but now that I think about it, perhaps an alternative to newtype wrappers could be specific imports of modules containing specific variations of instances.

Another alternative I thought about while writing this, is that maybe one could weaken the meaning of class (P x) => C x, where instead of it being a requirement that an instance of C selects an instance of P, we could just take it to loosely mean that for example, C class also contains P's methods but no instance of P is automatically selected, no other relationship with P exists. So we could keep some sort of weaker hierarchy that might be more flexible.

Thanks if you have some clarifications over that topic, and/or correct my possible misunderstandings.

1
I can’t give a definitive answer on this, so instead I’ll give a comment. Two points. One, it makes sense to have a hierarchy in many cases: a Monoid, both mathematically and in Haskell, must be a Semigroup (your Functor example is also a good one). Two, the problem of multiple type class definitions exists independently of inheritance. For example, you could make the Applicative instance for lists use the normal ap and the ZipList pure. Unfortunately, the laws we define for classes aren’t easily proved in standard haskell; however, breaking laws is an issue inheritance or not.cole
I'm not sure I understand some things you claim. AFAIU typeclass hierarchy in Haskell represents constraints in Maths. An Applicative Functor is necessarily a Functor in Maths. So the constraint Functor f => Applicative f represents exactly that: "Only Functors can also be Applicative Functors". I am also curious about you saying "c selects an instance of p". There is no such 'selection". What it means is "For x to be a C, x first needs to be a P". There is no choice involved.Sir4ur0n
But saying that a Monad is a Functor implicitly makes the choice of one particular Functor for that Monad. But that's just an expression of the way things are. Once you have a Monad, there is one and only one sensible Functor instance, given by fmap f x = x >>= (return . f) Robin Zigmond
I believe it's mostly about convenience. In the far past, we had Monad as an independent class from Functor (applicatives did not even exist), so we had to frequently write f :: (Monad m, Functor m) => ... if we wanted to reuse functions requiring a functor. That felt silly since fmap f x = x >>= return . f makes any monad into a functor.chi
I understand, for such simple things as a monad, there is really only one way to define a functor from it, and it feels very natural to regard a monad as a "more complex" thing than a functor. But my point is that for more complex structures perhaps there would be no clear natural hierarchy, there may be lots of different paths, I don't know.. So forcing one hierarchy creates a lot of asymmetry and bias in how to model some problem.jam

1 Answers

4
votes

Maybe you're tired of hearing from me, but here goes...

I think superclasses were introduced as a relatively minor and unimportant feature of type classes. In Wadler and Blott, 1988, they are briefly discussed in Section 6 where the example class Eq a => Num a is given. There, the only rationale offered is that it's annoying to have to write (Eq a, Num a) => ... in a function type when it should be "obvious" that data types that can be added, multiplied, and negated ought to be testable for equality as well. The superclass relationship allows "a convenient abbreviation".

(The unimportance of this feature is underscored by the fact that this example is so terrible. Modern Haskell doesn't have class Eq a => Num a because the logical justification for all Nums also being Eqs is so weak. The example class Eq a => Ord a would be been a lot more convincing.)

So, the base library implemented without any superclasses would look more or less the same. There would just be more logically superfluous constraints on function type signatures in both library and user code, and instead of fielding this question, I'd be fielding a beginner question about why:

leq :: (Ord a) => a -> a -> Bool
leq x y = x < y || x == y

doesn't type check.

To your point about superclasses forcing a particular hierarchy, you're missing your target.

This kind of "forcing" is actually a fundamental feature of type classes. Type classes are "opinionated by design", and in a given Haskell program (where "program" includes all the libraries, include base used by the program), there can be only one instance of a particular type class for a particular type. This property is referred to as coherence. (Even though there is a language extension IncohorentInstances, it is considered very dangerous and should only be used when all possible instances of a particular type class for a particular type are functionally equivalent.)

This design decision comes with certain costs, but it also comes with a number of benefits. Edward Kmett talks about this in detail in this video, starting at around 14:25. In particular, he compares Haskell's coherent-by-design type classes with Scala's incoherent-by-design implicits and contrasts the increased power that comes with the Scala approach with the reusability (and refactoring benefits) of "dumb data types" that comes with the Haskell approach.

So, there's enough room in the design space for both coherent type classes and incoherent implicits, and Haskell's appoach isn't necessarily the right one.

BUT, since Haskell has chosen coherent type classes, there's no "cost" to having a specific hierarchy:

class Functor a => Monad a

because, for a particular type, like [] or MyNewMonadDataType, there can only be one Monad and one Functor instance anyway. The superclass relationship introduces a requirement that any type with Monad instance must have Functor instance, but it doesn't restrict the choice of Functor instance because you never had a choice in the first place. Or rather, your choice was between having zero Functor [] instances and exactly one.

Note that this is separate from the question of whether or not there's only one reasonable Functor instance for a Monad type. In principle, we could define a law-violating data type with incompatible Functor and Monad instances. We'd still be restricted to using that one Functor MyType instance and that one Monad MyType instance throughout our program, whether or not Functor was a superclass of Monad.