If you are starting your Haskell journey I implore you to focus on the type system.
Types are the roots from which your Haskell knowledge grows.
Haskell has algebraic data types. This is a fancy way of saying that we can create types which are composites of other types. Intuitively we can think of types as sets of values.
Let's model a simplified demography of the fictional Blade Runner universe with algebraic data types.
Replicants
Replicants are bioengineered beings that are virtually identical to humans.
Let us define a type representing all models of replicant.
newtype Replicant = ReplicantModel SerialNumber deriving Show
Serial Number
is simply a string of characters which represents the unique ID of a given replicant model such as "N6MAA10816"
.
deriving Show
at the end there just means "automatically make the values printable".
Now we need a type to represents the identities of our replicants.
newtype SerialNumber = SerialNumber String deriving Show
When creating new types using the newtype
or data
keywords everything to the left of the =
in a type declaration lives in world of types. Whereas data constructors are located to the right of the =
and take other types as parameters.
In our type declaration SerialNumber
is a data constructor and has a parameter of type String.
Meaning give me a value of type String and I will construct a value for you of type SerialNumber
.
Data constructors can have no arguments in which case they are called "nullary" data constructors.
It can be confusing to see SerialNumber String
.
Whenever we declare a data constructor we always give the types of the arguments it takes.
When we want to actually use a data constructor we apply values to it.
> SerialNumber "N6FAB61216"
SerialNumber "N6FAB61216"
What's in a name? That which we call a rose by any other name would smell as sweet.
Okay so for a type declaration we list the types of arguments for data constructors and when we want
to actually construct data we apply values.
Types and values exist in different worlds
The first key to succeeding in any Haskell initiation ritual is to understand whether an expression is a type or a value judging by the context.
So every type declaration gives us a new type constructor and at least one new data constructor.
Bearing this in mind lets look at our Replicant
type again.
newtype Replicant = ReplicantModel SerialNumber deriving Show
Replicant
is a type constructor as it is on the left hand side of the =
. On the right hand of the side in the world of values we have ReplicantModel
which is a data constructor. Data constructors are just functions.
Let's make a serial number.
> SerialNumber
<interactive>: error:
• No instance for (Show (String -> SerialNumber))
arising from a use of ‘print’
(maybe you haven't applied a function to enough arguments?)
• In a stmt of an interactive GHCi command: print it
My mishap is a gift to your understanding.
Without a concrete value we don't have anything we can print. We only get concrete values from the evaluation of fully applied data constructors.
The key here in that message is that the data constructor SerialNumber
is only partially applied meaning it has the type of a function.
Data constructors are just functions.
Specifically the type signature of the partially applied SerialNumber
is
String -> SerialNumber
The data constructor expects a value of type string to be applied to the SerialNumber
data constructor.
In this context "applied" means that all the parameters to have been given concrete values. In contrast to my mishap where SerialNumber was partially applied.
For example if we have
> add :: Int -> Int -> Int
> add a b = a + b
Then we can partially apply add
> let x = add 1
Note we can't print functions so we have to bind it to a variable instead.
or we can fully apply add
> add 1 1
2
Just like normal functions, data constructors have can have values "applied" to their parameters too.
People versed with other weird/normal languages usually refer to this as calling a function with an argument but forget what you know.
Let's birth this new replicant model into the world of values.
> ReplicantModel (SerialNumber "KD6-3.7")
ReplicantModel (SerialNumber "KD6-3.7")
Humans
While we are at it we need a type to identify humans.
It is customary to give humans a pronouncable name.
newtype Name = Name String deriving Show
Pay close attention to the fact that both of our type and data constructors are both called "Name".
Don't let this confuse you.
The Haskell compiler can infer from the context whether you are in the values world or the types world and will decide which one to use whenever "Replicant" or "Human" is evaluated.
On the left Name
is a type constructor which represents types. The right hand side has a data constructor, also called Name
which represents values.
This happens a lot in Haskell and confuses those starting out.
Now lets make a type to represent humans. Here the Name
type constructor to delineate the types of values we need to apply to the Person
data constructor.
newtype Human = Person Name deriving Show
In order to get a fully applied Person value of type Human
which we can actually use we apply a value of type Name to our Person
data constructor.
> Person (Name "Deckard")
Person (Name "Deckard"
Now we have fully applied our data constructor we have a value of type Human we can actually use.
We can verify that the type is human with the :t
command in the Haskell interpreter.
> :t Person (Name "Deckard")
Human
Citizens
We can now form a new composite type to represent the set of all species.
data Species = ReplicantSpecies Replicant | HumanSpecies Human deriving Show
Since we have two species Replicant
and Human
, the Species type has two data constructors. Note the |
in between them. This intuitively means "either".
In plain english a value of type Species is made up from a value of either type Replicant
or Human
.
Time to classify citizens according to their species and their birth year like any caring state does.
data Citizen = Citizen Species BirthYear deriving Show
Our Citizen
type has one data constructor also called Citizen
which has two parameters. The first of which takes a value of type Species
. The second parameter takes a value of type BirthYear
You might guess what is coming next.
newtype BirthYear = Year Int deriving Show
So there we have it. Lets create our first citizen on this lonely planet.
> let replicant = ReplicantSpecies (ReplicantModel (SerialNumber "LUV"))
> Citizen replicant (Year 2035)
Citizen (ReplicantSpecies (ReplicantModel (SerialNumber "LUV"))) (Year 2035)
"He named you. Must be special." - K
> let species = ReplicantSpecies (ReplicantModel (SerialNumber "LUV"))
> :t species
Replicant
> :t Citizen species (Year 2035)
Citizen
There we have it. The best way to learn about the type system is to model your own domains with algebraic data types and play around with them. Use the :t
command Haskell ghci
repl to see the types of expressions.
The next steps are to understand the difference between product versus sum types and what kinds are in Haskell. Grasping both of these ideas will enhance your understanding of this article.
The code in this article is available in the online REPL here.
Top comments (2)
This seems to be incorrect:
replicant
can't be applied to the year since it isn't a functionThanks for pointing that out. I had included the function in the code I linked to but forgot to include it in the article.