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).

If you ask 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.

Again, it might be useful to display these data types so add 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 Ints, 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.

If you use 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)]
Note that we used the more lightweight 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.