DEV Community

Tom Chambrier
Tom Chambrier

Posted on • Updated on

Modelling the World of Blade Runner with Haskell's Type System

alt text

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)

Collapse
 
neilmayhew profile image
Neil Mayhew

This seems to be incorrect:

:t replicant (Year 2035)

replicant can't be applied to the year since it isn't a function

Collapse
 
therewillbecode profile image
Tom Chambrier

Thanks for pointing that out. I had included the function in the code I linked to but forgot to include it in the article.