Intro to Haskell

Updated: 28 November 2024

Notes based on Learn you a Haskell

Prerequisites

Setup a Haskell Environment as per the Getting Started Docs as well as install the Haskell extension for your editor

This installation process is a little slow, but stay with it I guess. (it’s still easier than the Ocaml install)

Syntax Basics

To write the first bits of Haskell we can use the REPL which, once installed, can be accessed with the ghci command

Math can be done in a fairly normal fashion:

1
gchi> 5 + 17
2
22
3
4
gchi> 5 * 42
5
210
6
7
gchi> 5 / 2
8
2.5

And booleans similarly:

1
ghci> True && False
2
False
3
4
gchi> not False
5
True

And equality:

1
ghci> 5 == 5
2
True
3
4
gchi> 5 == 10
5
False
6
7
gchi> 5 /= 10
8
True
9
10
gchi> "hello" /= "world"
11
True

All these basic operators are functions in Haskell that work using infix notation. We can also refer to them purely as a function by wrapping them in brackets, e.g. (+) or (==)

Functions can be called using the name of the function followed by the arguments, separated by spaces

1
gchi> succ 5
2
6

Or with multiple arguments

1
ghci> min 9 10
2
9

If a function takes two parameters we can call it using infix notation with backticks surrounding the function name, for example the min function:

1
ghci> 9 `min` 10
2
9

Functions

We can define our own functions using a syntax like:

double.hs
1
doubleMe x = x + x

We can put that into a file, e.g. called double.hs, and load it into the REPL with :l:

1
ghci> :l double
2
[1 of 2] Compiling Main ( double.hs, interpreted )
3
Ok, one module loaded.

We can then use the function that was defined

1
ghci> doubleMe 5
2
10

A function that takes multiple arguments can be defined by separating the arguments by a space

double.hs
1
doubleMe x = x + x
2
3
doubleUs x y = doubleMe x + doubleMe y

In the above cases, Haskell is able to determine the types of our function as: doubleUs:: Num a => a -> a -> a

We can make a function that does some conditional stuff using thenif .. then .. else expression

double.hs
1
doubleSmall x =
2
if x < 100
3
then x * 2
4
else x

Note this is not whitespace sensitive

Functions that take no parameters are also called definitions or names, we can defined a function that is equal to some string value as follows:

1
bobsName = "Bob Smith"

Function names must start with a lowercase letter and can contain a ', e.g. bob'sName = "BobSmith"

Lists

Lists store elements of the same type. Lists can be defined using [] with elements separated by commas

1
myList = [1, 2, 3, 4]

They can also be concattenated with ++

1
biggerList = [1, 2, 3, 4] ++ [5, 6]

++ can also be used with strings:

1
"hello" ++ " " ++ "world"

When concatenating elements Haskell needs to traverse the entire list which can negatively impact performance. An alternative way of adding an item to a list is the the cons (:) operator:

1
ghci> 1 : [2, 3, 4]

Elements can be accessed from a list using !!

1
ghci> [1, 2, 3, 4] !! 2
2
3

Lists can also be compared. Comparison of lists are done item by item, for example a list

1
ghci> [1,2,3] > [4,5,6]
2
False
3
ghci> [1,2,3] > [0,1,2]
4
True

Some functions for working with lists are:

  • head - gets the first item
  • tail - gets everything other than the first element
  • last - gets the last item
  • init - gets everything other than the last element
  • length - gets the length of a list
  • null - checks if the list is empty
  • reverse - reverses a list
  • take - takes a certain number of elements from a list
  • drop - drops a certain number of elements from a list
  • minimum - gets the smallest element of a list
  • maximum - gets the largest element of a list
  • sum - adds up items of a list
  • product - multiplies all numbers in a list
  • elem - checks if an element is in a list, usually called as an infix method since it’s more readable that way
  • replicate - produces a list by repeating some value a certain number of times

Calling head, tail, last, or init on an empty list will throw an error

Ranges

Ranges allow us to define sequences of enumerable values. Ranges can be defined using a start and end value

1
ghci> [1..10]
2
[1,2,3,4,5,6,7,8,9,10]
3
4
ghci> ['a'..'k']
5
"abcdefghijk"

They can even be defined using the first two values in the case where we want to use a step larger than 1

1
ghci> [1,3..20]
2
[1,3,5,7,9,11,13,15,17,19]
3
4
ghci> ['a','d'..'z']
5
"adgjmpsvy"

Additionally, ranges can also be infinite by not specifying the end of the range. We can see the result of doing this by taking some elements from the range as below:

1
ghci> take 10 [1..]
2
[1,2,3,4,5,6,7,8,9,10]
3
4
ghci> take 30 ['a'..]
5
"abcdefghijklmnopqrstuvwxyz{|}~"

There are also a few useful functions for working with ranges such as:

  • cycle - which repeats the elements of a given list to produce an infinite range
  • repeat - creates an infinite range of a repeated value

List Comprehensions

List comprehensions are like Set Comprehensions in math and allow us to define some general sets using a set of predicates

A set comprehension follows the structure of [ expression | variable <- values, predicate]. This is more clear in practice:

1
ghci> [x^3 - 1 | x <- [1..5]]
2
[0,7,26,63,124]
3
4
ghci> [x^3 - 1 | x <- [1..10], x `mod` 2 == 0]
5
[7,63,215,511,999]
6
7
ghci> [x^3 - 1 | x <- [1..10], x `mod` 2 /= 0]
8
[0,26,124,342,728]

These can also be used with multiple variables in order to produce permutations of a multi-variable expression

1
ghci> [a ++ " " ++ b | a <- ["hello", "hi"], b <- ["world", "bob"]]
2
["hello world","hello bob","hi world","hi bob"]

Tuples

Tuples work pretty much the same as in other languages. A tuple lets us store multiple values together. Items in a tuple can be different types, we can use this as we’d expect

1
ghci> (1, 'a')
2
(1,'a')
3
4
ghci> [(1, 'a'), (2, 'b')]
5
[(1,'a'),(2,'b')]
6
7
ghci> [(x, y) | x <- [1..3], y <- ['a'..'c']]
8
[(1,'a'),(1,'b'),(1,'c'),(2,'a'),(2,'b'),(2,'c'),(3,'a'),(3,'b'),(3,'c')]

When a tuple has only two elements we refer to it as a pair. Pairs have some methods for working with them, namely:

  • fst - gets the first item from a tuple
  • snd - gets the second item from a tuple

It’s not too difficult to implement these functions, an example implementation may look like so:

1
fst' (x,_) = x
2
3
snd' (_,y) = y

And we can compare our usage with the base implementation

1
ghci> fst' (1,2)
2
1
3
ghci> snd' (1,2)
4
2
5
6
ghci> fst (1,2)
7
1
8
ghci> snd (1,2)
9
2

Another nice function for working with tuples is zip which takes two lists and iterates over them and outputs a list of tuples

1
ghci> zip [1,2,3] ['a','b','c']
2
[(1,'a'),(2,'b'),(3,'c')]

Types

Haskell is statically typed. Thus far we haven’t had to annotate the types of our functions since the language is pretty good at inferring the types of stuff

We can use :t in the REPL to view the type of something

1
ghci> :t 5
2
5 :: Num a => a
3
4
ghci> :t 'b'
5
'b' :: Char
6
7
ghci> :t "Hello"
8
"Hello" :: String

We can specify the types of a function by writing it above a function implementation, for example:

1
doubleMe :: Int -> Int
2
doubleMe x = x + x

Type Variables

We can look at the types of some more complex values

1
ghci> :t fst
2
fst :: (a, b) -> a
3
4
ghci> :t (+)
5
(+) :: Num a => a -> a -> a
6
7
ghci> :t (==)
8
(==) :: Eq a => a -> a -> Bool

In the above functions, we can also see that some functions make use of a type variable, these types are denoted by the lowercase character in the type signature as in fst :: (a, b) -> a.

We can also see that functions can specify a constraint on their type variables, for example (+) :: Num a => a -> a -> a constrains a to be of type Num. The constraint is denoted before the => symbol in a type definition

Lastly, it’s also important to note that functions don’t differentiate between multiple inputs an outputs. So a function that takes multiple input just uses -> to separate values, this is because functions can partially apply them which yields another functions

Type Classes

A typeclass is like an interface that defines some kind of behavior. (They seem a bit like traits in Rust)

Some common typeclasses are:

  • Eq - which makes something comparable with == or /=
  • Ord - used for ordering, makes something comparable with >, <, or ==. So implementation of Ord requires Eq as well
  • Show - means that something can be printed using show
  • Read - means that something can be read from a String into some type with the read function

The read function is interesting because it lets us read a value into multiple different types. In cases where this type cannot be inferred you can specify the type that something needs to be read into using :: Type which lets us provide a value to a type variable

1
ghci> read "7" :: Int
2
7
3
4
ghci> read "7" :: Float
5
7.0
6
7
ghci> read "[1,2,3]" :: [Float]
8
[1.0,2.0,3.0]

Additionally, we also have some number types:

  • Num - the base number type
  • Int - a bounded integer
  • Integer - a non-bounded integer
  • Float - a single precision, floating point number
  • Double - a double precision, floating point number
  • Enum - an enumerable value, these can be used in ranges

When working with numbers the fromIntegral function can be used to convert between number types, namely swapping between floating point and integer number types which is sometimes needed

Functions

Functions in Haskell have some interesting functionality when compared to most other languages. The first of this is how we can define different implementations for functions using pattern matching

Pattern Matching

Pattern matching when defining a function is done by essentially providing multiple implementations for a function that match to the same type signature but different concrete parameters. For example, the below function reacts differently depending on the name that is provided:

1
sayHi :: String -> String
2
sayHi "Bob" = "Good morning, sir!"
3
sayHi name = "Hello " ++ name

And when using this we can see how the inputs are handled:

1
ghci> sayHi "Jeff"
2
"Hello Jeff"
3
4
ghci> sayHi "Bob"
5
"Good morning, sir!"

We can use this idea to implement something like an implementation of the triangular number calculation that uses a but differentiating the recursive base case as a pattern

1
triangleNum :: (Integral a) => a -> a
2
triangleNum 0 = 0
3
triangleNum n = n + triangleNum (n - 1)

And we can use that to get the first 10 triangular numbers:

1
ghci> [triangleNum x | x <- [1..10]]
2
[1,3,6,10,15,21,28,36,45,55]

Pattern matching can also be done in more interesting ways, for example we can use it with tuples like so:

1
isFirstOne :: (Integral a, Integral b) => (a, b) -> String
2
isFirstOne (1, _) = "Yes"
3
isFirstOne (_, _) = "No"

And similarly for lists, for example:

1
tryFirstTwo :: [a] -> [a]
2
tryFirstTwo (x : y : xs) = [x, y]
3
tryFirstTwo (x : xs) = [x]
4
tryFirstTwo [] = []

When defining patterns, it’s important to keep the most specific patterns first, since they are matched in order (the compiler will warn you if your order is incorrect so that’s nice). Note that non-exhaustive patterns will be a runtime error so it’s important to ensure that we handle these as well

Another nice benefit of pattern matching is that we can use it for destructuring elements, for example with a tuple

1
addPair :: (Num a) => (a, a) -> a
2
addPair (x, y) = x + y

Guards

Guards allow us to branch based on a condition. They’re very similar to if/else statements and are evaluated in order

A simple guard can check some conditions:

1
howBig num
2
| num <= 0 = "Nothing really"
3
| num < 1 = "Really small"
4
| num >= 10 = "Wow that's a big number"
5
| otherwise = "Nothing notable here"

Where Bindings

Where bindings let us define local values within a function and can be done by using where with each binding on a different line:

1
f x = m * x + c
2
where
3
m = 10
4
c = 20

These are also usually indented for readability

Where bindings can also pattern match just as we would when defining a function:

1
f x = m * x' + c
2
where
3
(m, c) = (10, 20)
4
x' = sqrt x

Let Expressions

These are very similar to where bindings but can be used anywhere and are expressions in themselves, the values in the let part are accessible in the in part

Otherwise, they work as you’d expect

1
f x =
2
let
3
m = 10
4
c = 20
5
in m * x + c

Or with pattern matching:

1
f x =
2
let m = 10
3
c = 20
4
x' = sqrt x
5
in m * x' + c

An important distinction is that let expressions can be used in any place where we may need an expression, and evaluate to the in part

1
f xs =
2
[ let m = 10
3
c = 20
4
in m * x + c
5
| x <- xs
6
]

You can even define functions in them, of course:

1
f xs =
2
[ let f' x = 10 * x + 20
3
in f' x
4
| x <- xs
5
]

You can also use it in the predicate section of a list comprehension and do stuff with that:

1
f xs =
2
[y | x <- xs, let y = 10 * x + 20, y > 30.0]

This seems a bit syntax-dense to me, but I see why being able to refer to the value bound in the let expression could be useful

Case Expression

Case expressions let us pattern match within a function and get the resulting expression. This is pretty much as you’d expect as well

1
nameOf c = case c of
2
'a' -> "Alice"
3
'b' -> "Bob"
4
c -> [c]

Pattern matching in function definitions is actually just syntax sugar for case expressions so you can define the name function using a where binding with which would be the same as a case expression:

1
hi c = "Hi " ++ name c
2
where
3
name 'a' = "Alice"
4
name 'b' = "Bob"
5
name c = [c]

Recursion

Recursion works as normal. A nice benefit we have in Haskell is that we can define the recursive base case as a pattern definition:

1
biggest [] = error "list is empty"
2
biggest [x] = x
3
biggest (x : xs)
4
| x > rest = x
5
| otherwise = rest
6
where
7
rest = biggest xs

We can also define infinitely recursive functions, for example:

1
ones = 1 : ones

And because Haskell supports infinite lists, we don’t have to have an escape condition. This is useful and can be composed with other things, for example:

1
ghci> take 5 ones
2
[1,1,1,1,1]

And that just works as expected

1
quicksort :: (Ord a) => [a] -> [a]
2
quicksort [] = []
3
quicksort (x : xs) =
4
quicksort smaller ++ [x] ++ quicksort bigger
5
where
6
smaller = [a | a <- xs, a <= x]
7
bigger = [a | a <- xs, a > x]

Higher Order Functions

Functions that can take a function as parameters or return another function are called higher order functions

Curried Functions

Every function in Haskell only takes a single parameter. Functions that look like they take multiple parameters are in fact functions that return functions that take the remaining parameters. We can call these curried functions

An example of this is the max function, it can be used in the following two equivalent manners:

1
ghci> max 1 2
2
2
3
4
ghci> (max 1) 2
5
2

This is because max 1 returns a function that takes the second value. This idea lets us partially apply functions that require multiple parameters, for example:

1
triple :: a -> b -> c -> (a, b, c)
2
triple a b c = (a, b, c)
3
4
with1 :: b -> c -> (Integer, b, c)
5
with1 = triple 1
6
7
with12 :: c -> (Integer, Integer, c)
8
with12 = triple 1 2

In the above, we can see that the with1 and with2 are partially applied versions of the triple function in which a or a and b are provided

Infix functions can also be applied partially using sections - a section is done by surrounding the function with parenthesis and putting a parameter on one side. This means that we can also apply it differently depending on how we want to use it, for example:

1
divByTwo = (/ 2)
2
3
twoDivBy = (2 /)

It’s clear when we use it how this works:

1
ghci> divByTwo 10
2
5.0
3
4
ghci> twoDivBy 10
5
0.2

Higher Order Functions

We can define higher order functions as normal functions

1
applyTwice :: (t -> t) -> t -> t
2
applyTwice f x = f (f x)

We can then provide it with a function:

1
excited s = s ++ "!"
2
veryExcited = applyTwice excited

A very useful higher order function is flip, which is basically flips the arguments to a function:

1
flip f x y = f y x

Maps and Filters

Two common higher order functions are map and filter

  • map - applies a function to each value of a list
  • filter - returns a list with only items that return true from a predicate function

These functions are defined in the standard library. A basic definition for them might look as follows:

1
map' :: (a -> b) -> [a] -> [b]
2
map' _ [] = []
3
map' f (x : xs) = f x : map' f xs
4
5
filter' :: (a -> Bool) -> [a] -> [a]
6
filter' f (x : xs)
7
| f x = x : filter' f xs
8
| otherwise = filter' f xs

Or even using list comprehension:

1
map' f xs = [f x | x <- xs]
2
3
filter' f xs = [x | x <- xs, f x]

And using it as such:

1
ghci> map' (*2) [1,2,3,4]
2
[2,4,6,8]
3
4
ghci> filter' even [1,2,3,4]
5
[2,4]

As we can see from the implementation, this behavior can be done using list comprehension pretty much directly. The usage of each really just depends on what is more readable ina given scenario

Since Haskell is lazy, mapping or filtering lists multiple times still only iterates through the list once

Lambdas

When working with higher order functions we often have a function that we’d just like to use once, to do this we can define a lambda which is an anonymous function. They are defined using the \p1 p2 -> expression

For example:

1
\x -> x + 5

Often we need to surround them in parenthesis since they will extend to the end of the line otherwise. Using it is done as follows:

1
ghci> map (\x -> x + 5) [1..5]
2
[6,7,8,9,10]

Folds and Scans

When working with recursion we often run into an edge case with the empty list, this is a pretty common pattern called folding. Folds reduce a list to some single value

We can use the fold-left method foldl to implement a sum function:

1
sum' xs = foldl (\acc x -> acc + x) 0 xs

foldl takes a lambda to handle the accumulation, the initial value, and the array. Note that for the above case in which we’re writing the sum function, it can be replaced more simply as:

1
sum' :: (Num a) => [a] -> a
2
sum' = foldl (+) 0

The same idea can be applied to the elem method:

1
elem' y = foldl (\acc x -> x == y || acc) False

Equally, you can use a fold-right which does the same but iterates from the right. Additionally, the lambda arguments are flipped around:

1
elem' y = foldr (\x acc -> x == y || acc) False

Folds are pretty powerful and can be used to implement lots of other standard library functions

A nice compositional example is how we can use these methods with flip to do something like reverse a list:

1
reverse' xs = foldl (flip (:)) [] xs

Function Application

Function application is done using the $ operator and is defined as such:

1
($) :: (a -> b) -> a -> b

The purpose of this is to reduce the precedence of function application. This means that instead of doing e(f(g x)) we can do e $ f $ g x

So we can rewrite something like:

1
ghci> sum (filter (> 10) (21:[7..15]))
2
86

As

1
ghci> sum $ filter (> 10) $ 21:[7..15]
2
86

Thus, effectively reducing the precedence of function application

Additionally, this can also be used to apply a function dynamically, for example:

1
ghci> map ($ 10) [(1+), (2+), (3+)]
2
[11,12,13]

Function Composition

Function composition is defined mathematically as (f.g)(x) = f(g(x)). In Haskell this is the same:

1
(.) :: (b -> c) -> (a -> b) -> a -> c

Using this, we can convert:

1
ghci> negate (abs (product [-1, 5, 4]))
2
-20

Into:

1
ghci> (negate . abs . product) [-1, 5, 4]
2
-20

Or using what we just learnt about the function application:

1
ghci> negate . abs . product $ [-1, 5, 4]
2
-20

This mechanism also makes it possible to write functions in a more point-free style, for example:

1
f :: (Num a) => [a] -> a
2
f = negate . abs . product

Modules

A module is a collection of related functions and types. A program is a collection of modules

Modules can be imported in a few different ways, for the sake of example we will use some standard library modules

To import everything:

1
-- import everything from a module into the global scope
2
import Data.List

To import everything but require the fully qualified name:

1
-- import everything from a module into the global scope
2
import qualified Data.List
3
4
5
-- members must be accessed as Data.List.name

Import a module but provide an alias for qualifying:

1
-- everything from module is in scope as L
2
import qualified Data.List as L
3
4
5
-- members must be accessed as L.name

Import specific stuff:

1
-- only named imports will be in global scope
2
import Data.List (nub, sort)

Import everything except some stuff:

1
-- everything other than nub and sort will be in scope
2
import Data.List hiding (nub, sort)

Data.List

Has some useful methods for working with lists, some of these that are useful are:

In addition to the list functions from the Lists section, we also have:

  • intersperse - puts an element between list items
  • intercalate - puts an element between list items and then flattens the result
  • transpose - transposes a list of lists
  • fold1' and foldl1' - stricter versions of the lazy fold methods, used when the base fold implementation yields a stack overflow for large lists
  • concat - flattens a list of lists one level
  • concatMap - the same as mapping into a list and then concatenating the results
  • and - takes a list of booleans and returns if all items are True
  • or - takes a list of booleans and returns if any items are True
  • all - like and, but takes a predicate and a list
  • any - like or, but takes a predicate and a list
  • iterate - takes an iterator function and a starting value, returns an infinite list
  • splitAt - splits a list at that many items
  • takeWhile - takes all items of list while a predicate is True
  • dropWhile - drops all items of list while a predicate is True
  • span - returns a tuple of the result of takeWhile and dropWhile
  • break - splits a list where the predicate is first True (kind of the opposite of span)
  • sort - sorta a list of Ord
  • group - groups adjacent elements if they’re equal
  • inits - like init but recursively gets all growing lists from the start
  • tails - like tail but recursively gets all growing lists from the end
  • isInfixOf - is list contained in other list
  • isPrefixOf - list starts with other list
  • isSuffixOf - list ends with other list
  • isSubsequenceOf - list contains other list
  • partition - takes a predicate and separates items that meet the predicate from those that don’t
  • find - finds an element that meets a specific predicate, returns a Maybe
  • elemIndex - gets a Maybe index of the element in the list
  • elemIndices - gets a indices of the elements in the list
  • findIndex - gets a Maybe index of the element that satisfies a predicate
  • findIndices - gets list of indices of elements that satisfy predicate
  • zipWith3, zipWith4, etc. - zips a list of varying sizes
  • lines - splits string into lines
  • words - splits string into words
  • nub - deduplicates list
  • delete - deletes the first occurrence of element in list
  • \\ - removes all elements to the right from the left: left \\ right
  • union - returns a union of two lists, removes duplicates from the second list
  • intersect - intersection of two lists
  • insert - inserting into a sorted list keeps the list sorted
  • sortBy, groupBy, insertBy, maximumBy, etc. - take a function to determine the order of two elements and apply that to the relevant method

Data.Char

Functions for working with Chars

  • isControl
  • isSpace - any whitespace character
  • isLower
  • isUpper
  • isAlpha
  • isAlphaNum
  • isPrint
  • isDigit
  • isOctDigit
  • isHexDigit
  • isLetter
  • `isMark
  • isNumber
  • isPunctuation
  • isSymbol
  • isSeparator
  • isAscii
  • isAsciiUpper
  • isAsciiLower
  • digitToInt - converts a char to an int if it is in ranges 0..9, a..f or A..F
  • intToDigit - converts 0..15 to 0..9,a..f
  • ord - convert character to their number
  • chr - convert number to character

Data.Map

Methods for working with association lists or dictionaries

Maps are made from a list of tuples with a key and value

1
import qualified Data.Map as Map
2
3
myMap = Map.fromList [("a", 1), ("b", 2)]
  • Map.fromList - creates a map from a list of tuples
  • Map.empty - creates an empty map
  • Map.insert - inserts an item into a map
  • Map.null - checks if a map is empty
  • Map.size - gets the size of a mpa
  • Map.singleton - creates a singleton map from a key and value
  • Map.map - works like List.map
  • Map.filter - works like List.filter
  • Map.member - check if item is member of map
  • Map.toList - creates a list of key/value pairs
  • Map.fromListWith - like Map.fromList but takes a function for creating a map from the items for each key, since this can have duplicates
  • Map.insertWith - same as Map.fromListWith but for insertion
  • Map.lookup - gets the item from a map

Data.Set

All elements in a set are unique. Sets are implemented as trees and are ordered. Operations like inserting, deleting, checking for existence, are much faster than for lists

1
import qualified Data.Set as Set

Many of these methods are similar to their List or Map equivalents

  • Set.fromList
  • Set.difference
  • Set.null
  • Set.union
  • Set.singleton
  • Set.size
  • Set.insert
  • Set.isSubsetOf
  • Set.isProperSubsetOf
  • Set.filter

Defining a Module

When defining a module we specify the name of the module and then the items that it exports followed by where, for example:

QuickMath.hs
1
module QuickMath
2
( myAdd,
3
myDivide,
4
myMultiply,
5
mySubtract,
6
)
7
where
8
9
myAdd a b = a + b
10
11
mySubtract a b = a - b
12
13
myMultiply a b = a * b
14
15
myDivide a b = a / b

The name of the module file should also match name of the module, e.g. QuickMath.hs for the above

Custom Types and TypeClasses

Algebraic data types

Algebraic types can be defined using data Name = T1 | T2, for example, we can define a type of Food like so:

1
data Food = Cake Int | Cereal Float

However, if we try to print out our Food we have an error, in order to make it printable we need to make it Showable, that can be done as:

1
data Food = Cake Int | Cereal Float deriving (Show)

We can introduce intermediate types as well, e.g. for our flavours:

1
data Flavor = Chocolate | Plain deriving (Show)
2
3
data Food = Cake Flavor Int | Cereal Flavor Float deriving (Show)

We can make methods that work with the custom data types we defined as normal:

1
mass :: Food -> Float
2
mass (Cake _ slices) = 100.0 * fromIntegral slices
3
mass (Cereal _ weight) = weight
4
5
eat :: Food -> Food
6
eat (Cake n s) = Cake n (s - 1)
7
eat (Cereal n w) = Cereal n (w - 100)

So we can use this as:

1
food = Cake Chocolate 12
2
m = mass food

Records

If we want to have lots of properties in our type it would be nice if we could label them a little better, we can do that using records which are an alternative way to write data types:

1
data Ingredient = Ingredient
2
{ name :: String,
3
percentage :: Float
4
}
5
6
data Specs = Specs
7
{ food :: Food,
8
ingredients :: [Ingredient],
9
bestBefore :: String
10
}

And used as:

1
flour = Ingredient {name = "Flour", percentage = 60}
2
3
sugar = Ingredient {name = "Sugar", percentage = 30}
4
5
butter = Ingredient {name = "Butter", percentage = 10}
6
7
cake =
8
Specs
9
{ food = Cake Plain 10,
10
bestBefore = "Next week",
11
ingredients =
12
[ flour,
13
sugar,
14
butter
15
]
16
}

Type Parameters

Algebraic types can also type type parameters using the syntax as:

1
data Menu a b = Breakfast | Lunch a | Dinner b

We can have one or more parameters and use it as needed as we can see above

Derived Instances

As we’ve seen with deriving typeclasses, we can derive behavior for these types, they’re a little like interfaces or traits in other languages. Some typeclasses that we can derive are: Show, Read, Eq, Ord, Bounded, Enum

For a simple example, we can define a data type for our meals:

1
data Meal = Breakfast | MorningSnack | Lunch | AfternoonSnack | Dinner | MidnightSnack deriving (Show, Eq, Ord, Enum, Bounded)

And that can be used as:

1
ghci> minBound :: Meal
2
Breakfast
3
4
ghci> succ Lunch
5
AfternoonSnack
6
7
ghci> [Breakfast .. Dinner]
8
[Breakfast,MorningSnack,Lunch,AfternoonSnack,Dinner]

Type Synonyms

These provide a way for us to give an alternative name to an existing type, for example, rewriting our Food data type above:

1
type Slices = Int
2
3
type Weight = Float
4
5
data Food = Cake Flavor Slices | Cereal Flavor Weight deriving (Show)

This makes it a bit easier to understand the intention of the different fields in our type

We can also use these to define concrete types that specify type constructors (generic types):

1
data Menu a b = Breakfast | Lunch a | Dinner b
2
3
type HomeMenu = Menu Food Ingredient

Recursive Data Structures

Data structures can also be recursive, an example of this is how the List type is defined:

1
data List a = Empty | Cons a (List a)

We can define our own type for recipes recursively like so:

1
type Previous = String
2
3
data Recipe = Done | Recipe Previous Recipe deriving (Show)

Infix Function Definitions

Functions that are made up of only special characters are automatically infix. So we can define a function for composing recipes:

1
infixr 5 |->
2
3
(|->) :: Previous -> Recipe -> Recipe
4
a |-> b = Recipe a b

In the above, we see that we can also define the fixity of a function, this lets us specify the precedence of this function. The above function lets us defined a recipe in “reverse” as so:

1
x =
2
"Chop vegetables" |-> "Fry onion" |-> "Boil potatoes" |-> "Mix" |-> Done

The resulting recipe looks like so:

1
Recipe "Chop vegetables" (
2
Recipe "Fry onion" (
3
Recipe "Boil potatoes" (
4
Recipe "Mix" Done
5
)
6
)
7
)

Note this isn’t really a natural way to depict this, but for the purpose of depicting fixity it gives us something to work with

Typeclasses

As mentioned previously, typeclasses provide additional behavior to types, the Eq typeclass is defined in the prelude as:

1
class Eq a where
2
(==) :: a -> a -> Bool
3
(/=) :: a -> a -> Bool
4
x == y = not (x /= y)
5
x /= y = not (x == y)

In this definitions, equality is defined recursively.

We can also implement our own definition of equality by ensuring we have implemented all the relevant function definitions. For the sake of equality we only need to implement one of the definitions since they’re defined recursively

We can implement equality for two food types provided their mass is the same, so:

1
instance Eq Food where
2
(==) a b = mass a == mass b

Or, alternatively, written in infix notation:

1
instance Eq Food where
2
a == b = mass a == mass b

We can also implement Show:

1
instance Show Food where
2
show (Cake Chocolate _) = "a delicious chocolate cake"
3
show (Cereal Chocolate _) = "some delicious cereal cake"
4
show _ = "ew gross"

Note that in the where we can simply define the show function using some pattern matching. We can also define this externally and reuse it inside our typeclass instance:

1
showFood :: Food -> String
2
showFood (Cake Chocolate _) = "a delicious chocolate cake"
3
showFood (Cereal Chocolate _) = "some delicious cereal cake"
4
showFood _ = "ew gross"
5
6
instance Show Food where
7
-- reference the existing showFood function
8
show = showFood

Since this has been defined, the Food type no longer needs to derive Show on it’s own since it has a concrete implementation

It’s also possible to derive for type constructors, for example, you could derive equality for Maybe like:

1
instance (Eq m) => Eq (Maybe m) where
2
Just x == Just y = x == y
3
Nothing == Nothing = True
4
_ == _ = False

Note that since we are comparing x == y we need to ensure that the types are Eq, so we can only implement a typeclass for a type constructor provided that the given type implements Eq, this is similar to generic constraints in functions

The Functor Typeclass

The Functor typeclass is basically used for anything that can be mapped over, e.g. Lists implement this, this is defined as follows:

1
class Functor f where
2
fmap :: (a -> b) -> f a -> f b

For lists, map is defined simply as:

1
instance Functor [] where
2
fmap = map

We can implement Functor for Maybe as:

1
instance Functor Maybe where
2
fmap f (Just x) = Just (f x)
3
fmap f Nothing = Nothing

Kinds

Type constructors are like functions that work over types to get concrete types. A Kind is a way to talk about the “type of a type”, we can view the kind of something using :k in GHCI, for example:

1
ghci> :k Int
2
Int :: *
3
4
ghci> :k Maybe
5
Maybe :: * -> *
6
7
ghci> :k Functor
8
Functor :: (* -> *) -> Constraint

The * represents where a concrete type is required. For example Int returns a concrete type, whereas Maybe takes one concrete type and returns a concrete type