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
Functor
type class. - make use of
Maybe
andEither
to 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 2
will be parsed as((g .) f) 2
by 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.
show
is 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 HaskellString
is 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
toJS
function is overloaded based on the type context which can be automatically resolved by the compiler. For example, in the body of theJS Team
instance, the usetoJS members
is 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 b
introduces an "type level assumption" under which the use oftoJS
inmap toJS l
must be well-defined givenl
has type[b]
andJS b
has 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 section, 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
Functor
type class. - how to make use of
Maybe
andEither
to 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