5. Algebraic Datatypes in Haskell
There are multiple ways of defining new data types in Haskell. In this session, we will focus on Algebraic Data Types, which allow us to specify multiply constructors, with any number of arguments.
A new ADT can be defined using the keyword data
followed by the desired name (necessarily capitalized!), an equal sign and then all constructors (capitalized!) separated by a pipe |
:
data Ordering = LT | EQ | GT
This type is already defined in haskell and is used whenever comparisons are needed (its constructors stand for “less than”, “equal” and “greater than”). Think of how the standard C function strcmp
takes two strings and returns a value indicating their lexicographic order:
- a negative number if the first string comes first
- 0 if they are equal
- a positive number if the second string comes first
In Haskell, it would return an Ordering
.
1. Write a function myCompare
which takes two integers and returns an Ordering
indicating their relationship (there is already a function in Haskell, compare
, which does this for all types of numbers).
2. Write a function which takes an integer and a sorted (ascending) list of integers and inserts that number into the proper position so that the resulting list is still sorted.
insert 7 [1,5,22,86] -> [1,5,7,22,86]
3. Write a function insertSort
that makes use of insert
to implement insertion sort on a list.
We now have a sorting function. However, it is unsatisfactory, because it can only sort numbers in an ascending order. We cannot specify any other criteria (e.g. descending order, by number of prime factors, by number of digits). We will expand the functionality by employing the Ordering
data type and an auxiliary “comparator function” which takes two integers and returns an Ordering
.
4. Write a function insertBy
which takes a comparator function, an integer and a sorted list of integers (s.t. $ \forall i, j; i < j, comparator(l_i, l_j) \neq GT$) and uses the comparator to insert the element at the proper location.
5. Write a function insertSortBy
which insert-sorts a list, with the aid of a comparator function (there is a function in the Data.List
module named sortBy
that does the same thing, though it uses another sorting algorithm).
A data type's constructors can also take arguments; when defining the data type, we need to specify the types of these arguments:
data Point = Point Float Float
Note that the constructor here can have the same name as the data type, because they are very different things and could never cause confusion (they can never appear in the same contexts).
Once defined this way, we can use regular pattern matching for our constructors:
pointToPair :: Point -> (Float, Float) pointToPair (Point x y) = (x, y)
6. Write a function distance
which takes two points and computes their euclidian distance.
7. Write a function collinear
which takes three points and tells whether they are on the same line. (one way to do this is to compute the area of the triangle they define and make sure it's 0).
ghci
to print a Point
(e.g. by simply typing Point 2.0 3.1
at the prompt) it will throw an error, as it doesn't know how to do that. You can use the following definition to obtain a “default” representation.data Point = Point Float Float deriving Show
The meaning of deriving Show
will be discussed in a future session.
We can also define recursive data types, whose constructors can depend on another value of the same type.
data Natural = Zero | Succ Natural
Here, we say that a natural number is either the number zero, or the successor of another natural number (see Peano axioms). The number three would be Succ (Succ (Succ Zero)))
.
8. Write a function add
which takes two Natural
numbers and returns their sum (remember that you can do pattern matching to check the structure of a number).
9. Write a function mul
which takes two Natural
numbers and returns their product.
deriving Show
to the definition:data Natural = Zero | Succ Natural deriving Show
Using recursive data types, we can also define immutable lists of integers:
data IList = IVoid | ICons Int IList
However, we have seen that haskell lists are generic, capable of containing any type of element, not just integers. For this we need a polymorphic data type (as opposed to all the previous ones which are monomorphic). That is, a type that depends on another type.
data List a = Void | Cons a (List a)
In the above definition, we have mostly replaced Int
with a
. a
is a type variable; it can stand for any type (what makes it a type variable is that it starts with a lowercase letter; it could as well be b
or something
). We can now construct lists of any type. However, remember that all elements in a list must be of the same type; we can't combine integers and strings. That is why a
also follows the data type name, on the left hand side of the =
: if we have a List
which contains Int
s, then that is a List Int
.
10. Write a function myLength
which takes a List a
and returns its length. Notice how the type of the lists elements doesn't matter (and remember pattern matching!).
11. Write a function toHaskell
which converts our lists to haskell's lists. That is, it takes a List a
and returns a [a]
.
12. Define a recursive, polymorphic data type to represent binary trees. A binary tree is either the empty tree, or a node containing a value, a left subtree and a right subtree.
13. Write a function height
which takes a binary tree and returns its height.
14. Write a function size
which takes a binary tree and returns the number of nodes.
15. Write a function mirror
which takes a tree and mirrors it (for each node, the left subtree becomes the right subtree and vice-versa).
16. Write a function treeMap
which takes a function a → b
and a tree with values of type a
and applies the function on each of the tree's elements to yield a tree with values of type b
(exactly like map
for lists).
17. Write a function flatten
which takes a binary tree and collects all its values into a list (note that there are multiple ways to go through all the values, such as preorder, inorder, postorder; the choice is up to you).
Consider the following data type which defines a Student
, charactarized by a first name, a last name and a list of grades:
data Student = Student String String [Float]
18. Write a function avg
to compute the average of a student's grades.
length
on a list and want to divide to it, you'll notice haskell raises an error when asked to divide a Float
to an Int
; you can call fromIntegral
on an Int
to perform an explicit conversion.
19. Write a function studComp
which takes two students and returns an Ordering
based on their averages.
20. Write a function highestAverage
which takes a list of students and returns the name and surname (separated by a space) of the student with the highest average.
The following data type models simple arithmetic expressions, where we can have variables, integer constants and addition and multiplication:
data AExpr = Const Int | Var String | Add AExpr AExpr | Mul AExpr AExpr
Similarly, the following models simple boolean expressions that are either an equality/greather-than comparison between two arithmetic expressions or a negation of a boolean expressions:
data BExpr = Eq AExpr AExpr | Not BExpr | Gt AExpr AExpr
Our goal is to be able to evaluate an expression thus represented. First, we need a way to deal with variables: for this we define a context, which is a collection of mappings from a variable name to a value:
type Context = [(String, Int)]
type
to obtain a type synonym, because we don't actually need the power of ADTs.
21. Write a function search
which takes a Context
and a variable name and returns the value from that context (it is guaranteed, that exactly one value exists for each variable on which the function is called; you don't need to account for any edge-case).
22. Write a function evalA
which takes a Context
and an AExpr
and returns a single integer representing its evaluated value.
23. Write a function evalB
which takes a Context
and a BExpr
and returns a single boolean representing its evaluated value.