I am learning Haskell from learnyouahaskell.com. I am having trouble understanding type constructors and data constructors. For example, I don't really understand the difference between this:
data Car = Car { company :: String , model :: String , year :: Int } deriving (Show)
and this:
data Car a b c = Car { company :: a , model :: b , year :: c } deriving (Show)
I understand that the first is simply using one constructor (Car
) to built data of type Car
. I don't really understand the second one.
Also, how do data types defined like this:
data Color = Blue | Green | Red
fit into all of this?
From what I understand, the third example (Color
) is a type which can be in three states: Blue
, Green
or Red
. But that conflicts with how I understand the first two examples: is it that the type Car
can only be in one state, Car
, which can take various parameters to build? If so, how does the second example fit in?
Essentially, I am looking for an explanation that unifies the above three code examples/constructs.
Type and data type refer to exactly the same concept. The Haskell keywords type and data are different, though: data allows you to introduce a new algebraic data type, while type just makes a type synonym. See the Haskell wiki for details.
Type constructorOf any specific type a , be it Integer , Maybe String , or even Tree b , in which case it will be a tree of tree of b . The data type is polymorphic (and a is a type variable that is to be substituted by a specific type). So when used, the values will have types like Tree Int or Tree (Tree Boolean) .
In Haskell, every statement is considered as a mathematical expression and the category of this expression is called as a Type. You can say that "Type" is the data type of the expression used at compile time.
Maybe is a type constructor because it is used to construct new types (the result type depends on the type of a in Maybe a ), where such a type might be Maybe Int (notice, there's no type param a anymore, i.e. all type parameters are bound).
In a data
declaration, a type constructor is the thing on the left hand side of the equals sign. The data constructor(s) are the things on the right hand side of the equals sign. You use type constructors where a type is expected, and you use data constructors where a value is expected.
To make things simple, we can start with an example of a type that represents a colour.
data Colour = Red | Green | Blue
Here, we have three data constructors. Colour
is a type, and Green
is a constructor that contains a value of type Colour
. Similarly, Red
and Blue
are both constructors that construct values of type Colour
. We could imagine spicing it up though!
data Colour = RGB Int Int Int
We still have just the type Colour
, but RGB
is not a value – it's a function taking three Ints and returning a value! RGB
has the type
RGB :: Int -> Int -> Int -> Colour
RGB
is a data constructor that is a function taking some values as its arguments, and then uses those to construct a new value. If you have done any object-oriented programming, you should recognise this. In OOP, constructors also take some values as arguments and return a new value!
In this case, if we apply RGB
to three values, we get a colour value!
Prelude> RGB 12 92 27 #0c5c1b
We have constructed a value of type Colour
by applying the data constructor. A data constructor either contains a value like a variable would, or takes other values as its argument and creates a new value. If you have done previous programming, this concept shouldn't be very strange to you.
If you'd want to construct a binary tree to store String
s, you could imagine doing something like
data SBTree = Leaf String | Branch String SBTree SBTree
What we see here is a type SBTree
that contains two data constructors. In other words, there are two functions (namely Leaf
and Branch
) that will construct values of the SBTree
type. If you're not familiar with how binary trees work, just hang in there. You don't actually need to know how binary trees work, only that this one stores String
s in some way.
We also see that both data constructors take a String
argument – this is the String they are going to store in the tree.
But! What if we also wanted to be able to store Bool
, we'd have to create a new binary tree. It could look something like this:
data BBTree = Leaf Bool | Branch Bool BBTree BBTree
Both SBTree
and BBTree
are type constructors. But there's a glaring problem. Do you see how similar they are? That's a sign that you really want a parameter somewhere.
So we can do this:
data BTree a = Leaf a | Branch a (BTree a) (BTree a)
Now we introduce a type variable a
as a parameter to the type constructor. In this declaration, BTree
has become a function. It takes a type as its argument and it returns a new type.
It is important here to consider the difference between a concrete type (examples include
Int
,[Char]
andMaybe Bool
) which is a type that can be assigned to a value in your program, and a type constructor function which you need to feed a type to be able to be assigned to a value. A value can never be of type "list", because it needs to be a "list of something". In the same spirit, a value can never be of type "binary tree", because it needs to be a "binary tree storing something".
If we pass in, say, Bool
as an argument to BTree
, it returns the type BTree Bool
, which is a binary tree that stores Bool
s. Replace every occurrence of the type variable a
with the type Bool
, and you can see for yourself how it's true.
If you want to, you can view BTree
as a function with the kind
BTree :: * -> *
Kinds are somewhat like types – the *
indicates a concrete type, so we say BTree
is from a concrete type to a concrete type.
Step back here a moment and take note of the similarities.
A data constructor is a "function" that takes 0 or more values and gives you back a new value.
A type constructor is a "function" that takes 0 or more types and gives you back a new type.
Data constructors with parameters are cool if we want slight variations in our values – we put those variations in parameters and let the guy who creates the value decide what arguments they are going to put in. In the same sense, type constructors with parameters are cool if we want slight variations in our types! We put those variations as parameters and let the guy who creates the type decide what arguments they are going to put in.
As the home stretch here, we can consider the Maybe a
type. Its definition is
data Maybe a = Nothing | Just a
Here, Maybe
is a type constructor that returns a concrete type. Just
is a data constructor that returns a value. Nothing
is a data constructor that contains a value. If we look at the type of Just
, we see that
Just :: a -> Maybe a
In other words, Just
takes a value of type a
and returns a value of type Maybe a
. If we look at the kind of Maybe
, we see that
Maybe :: * -> *
In other words, Maybe
takes a concrete type and returns a concrete type.
Once again! The difference between a concrete type and a type constructor function. You cannot create a list of Maybe
s - if you try to execute
[] :: [Maybe]
you'll get an error. You can however create a list of Maybe Int
, or Maybe a
. That's because Maybe
is a type constructor function, but a list needs to contain values of a concrete type. Maybe Int
and Maybe a
are concrete types (or if you want, calls to type constructor functions that return concrete types.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With