If I define a class (the terminology is so confusing here, because Haskell's "class" is a type class and not an OO class) Base:
class Base where foo :: a -> StringI still can't create a list containing types that implement
Base
. My first instinct is just to write [Base]
but that is a type class and not a type. What I had meant is something like Base a => [a]
because type classes not types, they are just constraints on types.But it's worse than that, because type classes are also not subtypes. If I make two types DA, DB into "instance"s of Base, they still aren't "upcast"able:
> :type DA 3 DA 3 :: DA > :type DA 3 :: Base a => a Cannot unify the type-signature variable `a' with the type `DA' Expected type: a Inferred type: DA In the application `DA 3' When checking the type signature of the expression: DA 3 :: forall a. (Base a) => a
I think there are at least two ways to look at this: from an implementation sense, all of these type classes need to be resolvable at compile time. If I
foo
something, it needs to be known what function runs, which means that the thing getting foo
ed must be either a DA or a DB and that also must be known. Additionally, these DA and DB types are supposed to erase, I think?Another way to look at it is that type classes are just syntactic helpers, sorta, letting you use one name for a family of functions that behave similarly. I think it's most analogous to C++ templates and overloading.
What's interesting is that C++ effectively "infers" type classes.
Consider:
template<class T> void foo(T& t) { t.bar(); }
foo()
can only be called on classes that provide a bar
function. But that's not declared anywhere in foo's interface! So the compiler must look inside foo to see how it's used? If you just prototype foo
and define it elsewhere, you get linker errors...I guess that's showing C++'s lineage as some preprocessor hacks. Going back to Haskell, then, what they're letting me do is explicitly define in an interface what functions e.g. that "T" class has.
From that guy's notes: "There's a subtle distinction between multiple types that are interchangeable to the extent that they implement some common interface, and having a collection of different values that can be processed in similar ways, that my OO-infested mind cannot quite get clear."
So then the question becomes: how do you do "normal" dynamic dispatch? Google has some thoughts. I feel like I can use this and it sorta makes sense, but I really need to sit down and think hard about existential types and their relation to forall types. The GHC manual has a brief but good section describing it.