These notes are intended exclusively for the personal usage of the students of CS352 at Cal Poly Pomona. Any other usage is prohibited without previous written authorization. 1
2
The simplest way to create a new type in Haskell is to give a new name to an existing type. This is useful when we a complex type, often a tuple, is used in many places. For example the type Point is shorter to write and more meaningful than (Float, Float). 3
For example we can define a type of functions called Move that takes a float and a polygon and returns a new polygon. The vertical and horizontal move functions defined above would be of this type, but there could be others; Rotate comes to mind. 4
It is also possible to create parameterized types. Parameterized types are similar to generics in C# or Java. For example the type Predicate a denotes a function from any type to a Bool. The type Lookup k v represents list of key value pairs. 5
Type declarations are simple, one could say simplistic. They do not allow for recursive types, so one cannot define a new type Tree. Sometimes, as in the case of the Lookup type from the previous slide, one would like to use a context, e.g. to state that the key of a Lookup must belong to the Eq class. This too is illegal. Clearly a more powerful mechanism is needed, something that allow us to build more complex types. That is the Data declaration. 6
7
The Data declarations are like the Classes in Java or C#. To further the comparison, the name of the functions that build a value of such a type is called a Constructor. However the similitude cannot be pushed too far. While in Java/C# all constructors for a class have the same name, in Haskell all constructors must have a different name, regardless of the Data type. Remember that values in Haskell are immutable, that means every time we need a new value of the type we need to create it from a constructor. 8
Custom types regardless of the way they are declared (type or data) can be used just like any built-in type. 9
Just like the constructor of a class can have parameters and thus create new objects with different values, constructors in Haskell can have parameters as well and create different values. One way to think about that is as a way to encapsulate values. Since, once constructed, values never change, there is no need for any other representation, beyond the Constructor Param1 Param2 usual function call syntax. Furthermore one can use pattern matching to extract the original argument values. 10
Another way to put this is that Data declaration can be Generic (in the Java/C# sense). The type Maybe (also know as the Maybe Monad, but we ll see what that mean later in the course) is useful to formally encapsulate invalid data. C#/Java have the notion of null to handle this situation, but null is amorphic and this is a more formal and safer way to handle it. 11
12
(===) :: Nat -> Nat -> Bool (===) Zero Zero = True (===) _ Zero = False (===) Zero _ = False (===) (Succ x) (Succ y) = x === y (+++) :: Nat -> Nat -> Nat (+++) Zero x = x (+++) x Zero = x (+++) (Succ x) (y) = x +++ Succ y one, two, three, four :: Nat one = Succ Zero two = Succ one three = Succ two four = Succ three 13
14
15
16
17
18
19
20
We can define recursive functions on expression trees. The size function yields the number of nodes in the tree, and the eval function yields the result of traversing the tree and evaluating the result of the expression then the usual add and multiply semantics is applied to the Add and Mul nodes. 21
But Haskell is a functional programming language and it allows us to encapsulate the tree traversal into it s own function, which we call the fold function. We need to tell the fold function what to do when it finds a leaf and when it finds an inner node. For that we define two types: Unary represents a function of a single argument of type a that yields a result of type b. Binary represents a function that takes two arguments of type b and yield a result of type b. Armed with these two types, we can define the fold function for an Expr tree whose leaves are of type a. fold takes 4 arguments: - A tree whose leaves are of type a - A function that tells fold what to do for each leave (function from a to b) - A function that tells fold what to do for a node of type Add - A function that tells fold what to do for a node of type Mul Fold is surprisingly easy to write, it recursively traverses the tree and applies the appropriate functions. 22
Using fold size and eval are now one liners. Size counts every leave as 1 and yields the sum of the sizes of the left and right subtrees for any other node. Eval counts every leave as its integer value and applies addition or multiplication as expected for inner nodes. We can also easily define format, which yields a string human readable representation of the tree in the usual fully parenthesized arithmetic notation. 23
Remember that we have defined the Expr data type as deriving from the Eq class. This automatically gives an implementation for the == and the Show functions, thus allowing us to compare and display expression trees. But if we want to compare expr1 to expr2, the result is not what we would expect: they are not equal because they do not have the same shape (i.e. structure), even though they evaluate to the same value. 24
To work around this issue we need to redefine Expr as an instance of the Eq class and override the == function with a specialized implementation that compares the result of the evaluation of the two expression trees. Now expr1 is deemed equal to expr2. 25