Lecture 5: Polymorphism
Haskell has an expressive type system, which supports several kinds of polymorphism, i.e., ways to define and name functions so that the same function (or the same name) can meaningful at multiple types. This facilitates rich libraries and common vocabularies.
Polymorphic ADTs
Type variables can also be used to define polymorphic abstract data types, e.g.,
data Pair x y = Pair x y
In this case, we can have, e.g.,
Pair 1 "one" :: Pair Int String
or
Pair "one" 1.0 :: Pair String Double
Note here that these values are associated with types that do not have type variables. This is not a coincidence. All of the inhabited types of Haskell, i.e., all of the types for which there are values, cannot have type variables.
Type variables always begin with a lower-case letter, which distinguishes them from type constructors, which are either begin with upper-case letters, or in the binary case, can be infix-operators (cf., (:)
).
Indeed, a moment's reflection on the list type that we've been using all along is that it amounts to the following, module a bit of syntactic sugar:
data List x = Null
| Cons x (List x)
Polymorphic functions
Polymorphic ADTs naturally give rise to polymorphic functions, whose types include type variables. Consider, e.g., the definition of length
for a list:
length [] = 0
length (_:as) = 1 + length as
What is the type of length
? Well, in any specific context, it depends on the type of the list that it's going to be applied to. If we're applying length
to a [Int]
, then length
will have type [Int] -> Int
, whereas if we're applying it to a [Double]
, it has type [Double] -> Int
. But what is the type of length
at its definition, the type inferred by Haskell? We can find out using the :t
command in the interpreter:
> :t length
length :: [a] -> Int
Here, the type means that length
can take as input a list over any type, and compute its length.
Now, what about the type of map
? You might guess, based on the notion that map
takes a function and a list and returns a list, that it's type is
map :: (a -> a) -> [a] -> [a]
As it turns out, this is not as general as it could be. The actual type of map
is:
> :t map
map :: (a -> b) -> [a] -> [b]
Let's see an example. By way of preamble, if we have an ordinary binary function f
, we can turn it into an infix operator by enclosing it in backquotes, e.g., `f`
. This is often done for the div
function, which performs integral division,
> 5 `div` 2
2
But another reason to use backquotes is to set up a binary function for a section based on fixing its second argument. Here's an example:
> map (`replicate` '*') [1,2,3]
["*","**","***"]
Let's take a moment to understand this. The replicate
function is defined in Data.List
, and exported from there to the Prelude
, like most list functions. It takes an Int
and a value (of any type), and then builds a list with the given number of instances of that value. Thus, replicate 3 'a'
reduces to ['a','a','a']
, which is just "aaa"
. The section (`replicate` '*')
might be more easily understood by introducing a temporary function:
stars :: Int -> String
stars n = replicate n '*'
And then proceeding to the (all-but inevitable) η-reduction.
The stars
function builds a string of asterisks of the given length, i.e., stars 5
reduces to "*****"
.
*Exercise 5.1 What is the type of the occurrence of map
in the following?
map (`replicate` '*') [1,2,3]
Inferring the type of a complex polymorphic function can be tricky. It's often useful when you're getting started, to define the function first without a type ascription, and then to let Haskell infer the type itself via the :t
command in the interpreter. This type can then be cut-and-pasted back into the source. Still, remember that you are responsible for top-level type ascriptions in delivered code, so they must go there. And also make sure you understand the type!
Type classes
Haskell has another type of polymorphism, via type classes. If you're familiar with object oriented programming, the name here might be a bit misleading, suggesting that Haskell has a class system analogous to that of Java or C++. That's not actually the case. Haskell's type classes are more analogous to the notion of interface
from Java, or protocol
from Objective-C or Swift.
The kind of functional polymorphism we considered in the previous section is very much a monomorphic kind of polymorphism, in that while these functions may take different types of arguments, there is only one implementation, only one way of "doing it." That's not always enough. Let's consider a simple issue: the equality predicate (==)
. We'll want to be able to use equality as a binary predicate on values of very different types. Just one way of "doing" equality won't work. We need another mechanism:
class Eq a where
(==), (/=) :: a -> a -> Bool
x /= y = not (x == y)
x == y = not (x /= y)
This defines the type class Eq
, which contains two functions, equality (==)
, and inequality (/=)
. The declaration says that any type a
which is an instance of the Eq
type class must have binary predicates (==)
and (/=)
, i.e., functions of type a -> a -> Bool
. In addition, this class declaration provides default definitions of (==)
and (/=)
. It should be apparent that we can't successfully use both default definitions at the same time! But the effect is that an instance declaration need only define one, and we get a definition of the other “for free.” E.g., recalling our definition of Dollar
from Lecture 3:
data Dollar = Mill Integer
instance Eq Dollar where
Mill x == Mill y = x == y
type classes interact with the type system via constraints on the types of free variables in a type expression. For example, consider the Data.List
function elem
, which determines whether a value occurs in a list:
> 3 `elem` [1,2,3,2,3,4]
True
It's easy to implement elem
via a natural recursion:
elem _ [] = False
elem x (a:as)
| x == a = True
| otherwise = elem x as
or, more concisely
elem _ [] = False
elem x (a:as) = (x == a) || elem x as
relying on the left-biased definition of (||)
.
But it should be clear that the definition of elem
relies on the list elements supporting an equality test. How is this reflected in the type of elem
. Via a type constraint:
> :t elem
elem :: Eq a => a -> [a] -> Bool
What this type constraint means is that elem
can be used at type a -> [a] -> Bool
whenever a
is an instance of the Eq
type class.
Beginning with GHC 7.10, elem
has an even more general type:
> :t elem
elem :: (Eq a, Foldable t) => a -> t a -> Bool
Note here that the list type is an instance of the Foldable
type class, and that if we need to include multiple constraints, they're "tupled." We'll have more to say about Foldable
later.
One of the novel features of Haskell is that of derived instances. Here, a simple declaration will illustrate what's going on:
instance (Eq x, Eq y) => Eq (Pair x y) where
Pair x0 y0 == Pair x1 y2 = x0 == x1 && y0 == y1
Derived type classes are a powerful feature of Haskell, which enable us to add instances of polymorphic algebraic types to type classes. Here's a simple example that hints at much more:
instance (Eq a, Eq b) => Eq (a,b) where
(a0,b0) == (a1,b1) = a0 == a1 && b0 == b1
Note here that this is a very common case: equality for an ADT requires having the same constructor (in this case, Pair
), and equal components. This pattern happens so much that Haskell allows us to derive Eq
instances, cf.
data Pair x y = Pair x y
deriving (Eq,Show)
As this example hints, Show
is a type class too. Much of the expressiveness of Haskell comes from a particularly well designed set of type classes, which we will explore in the rest of the course.
*Exercise 5.2 Haskell's Prelude
defines an Ord
type class, which is relevant to the sorting examples from Lab 1. Read the documentation on Ord
, and then provide a parsimonious instance of Ord
for suitably type-constrained instances of Pair
.