50.054 - Parametric Polymorphism and Adhoc Polymorphism¶
Learning Outcomes¶
By this end of this lesson, you should be able to
- develop parametrically polymorphic Haskell code using Generic, Algebraic Datatype
- safely mix parametric polymoprhism with adhoc polymoprhism (overloading) using type classes
- develop generic programming style code using
Functortype class. - make use of
MaybeandEitherto handle and manipulate errors and exceptions.
Currying¶
In functional programming, we could rewrite a function with multiple arguments into a function that takes the first argument and returns another function that takes the remaining arguments.
For example,
can be rewritten into
These two functions are equivalent except that
- Their invocations are different, e.g.
- It is easier to reuse the curried version to define other function, e.g.
Function Composition¶
In math, let \(g\) and \(f\) be functions, then
In Haskell, there exists a prelude function (predefined function) (.) which composes two functions
For example
In Haskell, the symbols enclosed in a pair of parenthesis are user-defined infix operators. Hence (.) g f 2 in the above can be rewritten as
Note we have to put extra parathenses, since function applications are left associated, without the paranthesis
g . f 2will be parsed as((g .) f) 2by Haskell which is ill-typed.
Generics¶
Generics is also known as type variables. It enables a language to support parametric polymoprhism.
Polymorphic functions¶
Recall that the reverse function introduced in the last lesson
We argue that the same implementation should work for all lists regardless of their elements' type. Thus, we would replace Int by a type variable a.
Polymorphic Algebraic Datatype¶
Recall that the following Algebraic Datatype from the last lesson.
Same observation applies. MyList could have a generic element type a instead of Int and mapML should remains unchanged.
After the update, MyList does represent a type, but a type constructor. This is because
MyList itself is not a type, but MyList Int, MyList String or MyList a are types.
Type class¶
Suppose we would like to convert some of the Haskell values to JSON strings, we could rely on overloading.
showis a prelude function that converts values to string.
However the above is rejected by ghc.
We could give different names to the different versions of toJS but this s
This becomes hard to manage as we consider complex datatype.
For now, let's bear with this cumbersomeness and continue to extend our toJS funcitons to handle
the follwing data types
The second issue is that the personsToJS and contactsToJS are the identical modulo the variable names (and the types). Can we combine two into one?
The issue is partially resolved, because listToJS expects a function argument of type a -> String although
by specification, we want to restrict it to be one of the toJS functions we defined earlier, but we can't enforce it.
At this stage with have many different versions of toJS with different implementations and different shapes of type signature. It is a not a good approach to manage software.
One solution to address these issues is to use type class.
In the above, we define a type class JS via the class ... where keywords.
If this is the first time you encounter Haskell type class, you could treat it as the Haskell way of definining an interface in Java. In the above definition, we define an type class JS a which says whatever type a could be in JS a shoud have an obligational implementation of toJS :: a -> String.
The GHC pragma
{-# LANGUAGE FlexibleInstances #-}indicates that we need to enable the flexible-insances extension to supportJS String(which isJS [Char]). Without this pragma, we can't define complex type expression type class instances that involving a type constructor being applied to non type variables.
Using instance ... where keywords, we define some type class instances (concrete implementation) of JS a as follows
a in JS a with another more concreate type. In the body of the instance, we provide the concrete implementation of the toJS function with the specific type.
- One alarming thing is that the
JS [b]instance is overlapping withJS String, because in HaskellStringis a type alias of[Char]. Hence we argue thatJS [Char]is overlapping withJS [b]. Hence we need to add an instance pragma{-# OVERLAPS #-}to tell the ghc compiler to try applyJS [Char]whenever possible, otherwise, tryJS [b]. - Another "magical" thing of Haskell type class is that the use of the
toJSfunction is overloaded based on the type context which can be automatically resolved by the compiler. For example, in the body of theJS Teaminstance, the usetoJS membersis resolved to the isntanceJS [Person]which will be given by the instancesJS [b]andJS Person. - Thirdly, the
JS [b]instance, relies on a context, namelyJS b =>. The contextJS bintroduces an "type level assumption" under which the use oftoJSinmap toJS lmust be well-defined givenlhas type[b]andJS bhas been assumed existing.
Finally, we can test the code,
Note that when we call a function that requires a type class context, we do not need to provide the argument for the type class instance.
Type class enables us to develop modular and resusable codes. It is related to a topic of Generic Programming. In computer programming, generic programming refers to the coding approach which an instance of code is written once and used for many different types/instances of values/objects.
In the next few sections, we consider some common patterns in FP that are promoting generic programming.
Functor¶
Recall that we have a map method for list datatype.
Can we make map to work for other data type? For example
It turns out that extending map to different datatypes is similar to toJS function that we implemented earlier. We consider using a type class for this purpose. The following is a prelude type class in GHC.
In the above type class definition, t denotes a type parameter of kind * -> *. A kind is a type of types. In the above, * -> * it means Functor's argument must be a type constructor. For instance, it could be MyList or BTree and etc, but not Int and Bool. (C.f. In the type class JS, the type argument has kind *.)
The following is a prelude type class instance for Functor List (or as short-hand Functor [])
For the BTree data type, we need to provide the type class instance as follows,
Some examples
Functor Laws¶
All instances of functor must obey a set of mathematic laws for their computation to be predictable.
Let i be a functor instance
1. Identity: \i-> fmap \x->x i \(\equiv\) \y -> y. When performing the mapping operation, if the values in the functor are mapped to themselves, the result will be an unmodified functor.
2. Composition Morphism: \i-> fmap (f . g) i \(\equiv\) (\i -> fmap f i) . (\j -> fmap g j). If two sequential mapping operations are performed one after the other using two functions, the result should be the same as a single mapping operation with one function that is equivalent to applying the first function to the result of the second.
Foldable¶
Similarly we find a prelude type class Foldable for foldl and foldr operations
As for the BTree datatype, we need to define the instance as follows
Maybe and Either¶
Recall in the earlier lesson, we encountered the following example.
An error occurs when we try to evalue a MathExp which contains a division by zero sub-expression. Executing
Like other main stream languages, we could use try-catch statement to handle the exception.
One downside of this approach is that at compile type it is hard to track the unhandled exceptions.
A more fine-grained approach is to use algebraic datatype to "inform" the compiler (and other programmers who use this function and datatypes) that this function is not always producing an integer as the result.
Consider the following prelude Haskell datatype Maybe
When we execute eval (Div (Const 1) (Minus (Const 2) (Const 2))),
we get Nothing as the result instead of the exception. One advantage of this is that whoever is using eval function has to respect that its return type is Maybe Int instead of just Int therefore, a case pattern matching must be applied before using the result to look out for potential Nothing value.
There are still two drawbacks. Firstly, the updated version of the eval function is much more verbose compared to the original unsafe version. We will address this issue in the next lesson. Secondly, we lose the chance of reporting where the division by zero has occured. Let's address the second issue.
We could instead of using Maybe, use the Either datatype, which is also defined in the prelude.
We adjust the definition as follows
To make show (Div e1 e2) to works, we need to make the MathExp type as an instance of the Show type class or
a quick fix
Executing eval (Div (Const 1) (Minus (Const 2) (Const 2))) yields
Summary¶
In this lesson, we have discussed
- how to develop parametrically polymorphic haskell code using Generic, Algebraic Datatype
- how to safely mix parametric polymoprhism with adhoc polymoprhism (overloading) using type classes
- how to develop generic programming style code using
Functortype class. - how to make use of
MaybeandEitherto handle and manipulate errors and exceptions.
Appendix¶
Generalized Algebraic Data Type¶
Generalized Algebraic Data Type is an extension to Algebraic Data Type, in which each case extends a more specific version of the top level algebraic data type. Consider the following example.
Firstly, we need some type acrobatics to encode nature numbers on the level of type.
We use these two data types to encode natural numbers,
Next we define our GADT SList s a which is a generic list of elements a and with size s.
In the first subcase Nil, it is declared with the type of SList Zero a which indicates on type level that the list is empty. In the second case Cons, we define it to have the type SList (Succ n) a for some natural number n. This indicates on the type level that the list is non-empty.
Having these information lifted to the type level allows us to define a type safe head function.
Compiling head Nil yields a type error.
Similarly we can define a size-aware function snoc which add an element at the tail of a list.
If for some reason, we replace the 2nd clause of snoc with