Haskell has an incredibly expressive and powerful type system, but in understanding it there needs to be some mental concepts that one needs to get right. The key one (for me) is the very conscious and internalized distinction between what is a type constructor and what is a value (or data) constructor. I’ll use value constructor here though both terms represent the meaning just as well to me.
Here’s a very simple introduction. Basically, when we define a new data type, using the familiar “data” keyword, we are defining both a type constructor and a value constructor.
Haskell lets us define our own typeclasses and types.
Here’s a very simple example of a custom type (just ignore the deriving (Show) bit if you don’t already know it):
data StringString = StringString String String deriving (Show)
Let’s play with it:
:t StringString "aaa" "bbb" ==> StringString :t StringString "John" "Paul" ==> StringString
Note that the
StringString on the right of the = sign is the value constructor. Hence when we say
StringString “aaa” “bbb”, we are calling upon the value constructor to create a value, which “calls” (I’m being loose here) the type constructor at compile time to construct the type
StringString String String. Hence,
StringString is not a type, it’s a type constructor.
StringString String String is indeed a type (often called a concrete type), which was constructed via the type constructor
StringString. Also, note that we purposely chose to give the type constructor and value constructor the same name. It’s style, and it’s not necessary. It’s usually done only when there is only one value constructor for a given type.
We can also define a polymorphic type, where
a can mean anything. The term for
a is a type variable. That means it’s a variable, which holds a type – any type:
data StringAnything a = StringAnything String a deriving (Show)
Let’s play with it:
:t StringAnything "aaa" 123 ==> Num a => StringAnything a :t StringAnything "John" ["Jane"] ==> StringAnything [[Char]]
Note (this is important)
The value constructor references the type constructor to generate the type, using any type variables that are present in the value constructor to “pass” to the type constructor. Note that the value constructor need not use all of the type variables that are in the type constructor. But any type variables used in the value constructor must be seen by the type constructor, else it is out of scope and will result in a compile-time error. We actually saw some of these stuff in the first type variable in StringAnything already. Notice that the type of StringAnything is StringAnything a, where a is the type of the second parameter to its value constructor.
To make this really obvious, these are possible, and valid:
data SomeType1 a b c = SomeType1 deriving (Show) data SomeType2 a b c = SomeType2 a deriving (Show) data SomeType3 a b c = SomeType3 a b deriving (Show) data SomeType4 a b c = SomeType4 b a deriving (Show) data SomeType5 a b c = SomeType5 b a c deriving (Show) data SomeType6 c = SomeType6 Int String c deriving (Show)