Road to Haskeller #8 - Datatypes

Last Edited: 6/20/2024

The blog post introduces how to define datatypes in Haskell.

Haskell & Datatypes

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