The blog post introduces how to define datatypes in Haskell.

Datatypes
We have seen many predefined datatypes like lists, booleans, strings, and integers,
but we can define datatypes ourselves as well. Let's understand how to define a new
datatype by looking at how Bool
is defined.
--- Defining new datatype
data Name =
Constructor1 <args> | Constructor2 <args> ...
--- Name: name of a new datatype
--- Constructor 1,2,...: value constructors
--- <args>: arguments of constructors
data Bool = True | False
Bool
is a datatype and True
and False
are the value constructors. The value constructors
are functions that take 0 or more parameters and return values of a datatype. In the case of
True
and False
, they take no parameters of any type so they themselves are values of the
type Bool
.
As constructors are functions that can take 0 or more parameters, the following can be done:
data Calculation =
Add Int Int | Subtract Int Int | Multiply Int Int | Divide Int Int
All of the constructors in Calculation
take 2 values of type Int
and return a value of type Calculation
.
Let's see the types of the constructors using the :t command in GHCi.
ghci> :t Add
Add :: Int -> Int -> Calculation
ghci> :t Subtract
Add :: Int -> Int -> Calculation
As we can see from the above, the value constructors are functions, so we can do something like this too:
map (Add 1) [1,2,3] --- Output: [Add 1 1, Add 1 2, Add 1 3]
The Calculation
datatype and its constructors can be used for pattern matching like the following:
calc :: Calculation -> Int
calc (Add x y) = x + y
calc (Subtract x y) = x - y
calc (Multiply x y) = x * y
calc (Divide x y) = div x y
Recursive Datatypes
In Haskell, we need to learn to love recursion. The value constructors can be recursive meaning they can take a value of their type as well to make a new value. It should be easier to understand by looking at some examples:
data PeaNum =
Succ PeaNum | Zero
data Tree a =
Leaf | Node (Tree a) a (Tree a)
Both PeaNum
and Tree
have value constructors with no parameters (Zero
and Leaf
) as base case
and value constructors that take a value of their type as an input. This allows us to make more flexible datatypes.
Notice here that Tree
has a type variable a
in its definition. If you want to allow one type to be used
across all the components of that datatype but do not want to specify what type to be used, you can put a
type variable in its definition like in Tree
.
How do we use these recursive datatypes? Well, it is pretty much the same as normal datatypes:
--- Defining PeaNum value stored in a variable five
five = Succ $ Succ $ Succ $ Succ $ Succ Zero --- Using $ for syntax
--- Increment PeaNum
incr :: PeaNum -> PeaNum
incr = Succ --- Partial function application
--- Decrement PeaNum
decr :: PeaNum -> PeaNum
decr (Succ n) = n --- Decomposition of Succ
--- Adding two PeaNums
add :: PeaNum -> PeaNum -> PeaNum
add Zero n = n
add (Succ m) n = Succ $ add m n --- Decomposition of Succ and $ for syntax
--- Taking sum of PeaNums in a list
sumPeaNums :: [PeaNum] -> PeaNum
sumPeaNums [] = Zero
sumPeaNums (x:xs) = add x $ sumPeaNums xs --- using add function above
--- Convert to Int
count :: PeaNum -> Int
count Zero = 0
count (Succ n) = 1 + count n
There are many more rules regarding datatypes in Haskell, but let's call it a day for now.
Exercises
This is an exercise section where you can test your understanding of the material introduced in the article. I highly recommend solving these questions by yourself after reading the main part of the article. You can click on each question to see its answer.
Resources
- Philipp, Hagenlocher. 2020. Haskell for Imperative Programmers #10 - Datatypes. YouTube.
- NA. Making Our Own Types and Typeclasses. Learn You a Haskell for Great Good.