Haskell
Haskell supports both parametric and ad-hoc polymorphism.
Parametric Polymorphism
Parametric polymorphism is achieved the same way it is in OCaml, that is, when writing a function that does not rely on its parameters' types, then one can define the function's type with type parameters.
For example, the list length function:
length :: [a] -> Int
length [] = 0
length (x:xs) = 1 + length xs
where a
is the type parameter for the type of the list's elements.
Ad-hoc Polymorphism: Type Classes
According to the Haskell committee, there was no standard solution available for ad-hoc polymorphism when they designed the language, so type classes were developed and judged successful enough to be included in the Haskell design1.
In its simplest form, a type class is not much more than an interface as seen in object oriented languages, meaning it simply defines a named set of operations. For example, to define an interface for checking values for equality, one can define an Eq
type class with a class parameter a
, for which a (==)
function should exist:
class Eq a where
(==) :: a -> a -> bool
Mind that it's easy to misunderstand the a
class parameter as being similar to the a
type parameter used for parametric polymorphism, however this is not the case. Where in the parametric polymorphism case before, a
stood for any type, in this case a
stands for a class or set of specific types for which instances of Eq
exist.
The type class itself so far provides no functionality. We need to create an instance of the type class for a specific type, for example for Int
:
instance Eq Int where
(==) x y = eqInt x y
Similarly, an instance of the Eq
type class can be created for the specific type Float
:
instance Eq Float where
(==) x y = eqFloat x y
When we now try to use the (==)
function with parameters of types other than Int
or Float
, we will get a compile time error.
Type constraints
So far, we have seen how we can use type classes to overload functions defined within the type class, however we can also use them to define standalone functions with types implementing specific type classes.
Assume we want to define some kind of cost to our data types. We can use a type class to convey this idea:
class Cost a where
cost :: a -> Int
A possible instance for some custom data type could then look like this:
data Custom = Cheap | Expensive
instance Cost Custom where
cost Cheap = 0
cost Expensive = 1
Now say we want to define a function cheapest
that takes a list of elements of a type that defines a cost
, and then return the cheapest element of that list. Such a function could look like this:
cheapest :: Cost a => [a] -> a
cheapest [x] = x
cheapest (x:xs)
| cost x <= cost y = x
| otherwise = y
where y = cheapest xs
Note the function's type Cost a => [a] -> a
. It means that cheapest
is a function of type [a] -> a
for any type a
that is an instance of Cost
.
The similarities to parametric polymorphism are a lot more fitting in this case: a
for the cheapest
function is also a type parameter where the exact type does not matter for the function. The difference is that, whereas in the case of length
, any type can be inserted into a
, for cheapest
only types that implement a cost
can be used. This is necessary as the cost
function is used in the implementation of cheapest
.
Creating instances from other instances
One particular strength of type classes is that they allow one to create instances based on other existing type class instances. We extend the example from the "Type constraints" section to illustrate this: We are given a list of Custom
s and want to determine the overall sum of the list's costs. We could write an instance for Cost [Custom]
, however this seems rather verbose, considering we might want to have an implementation of this function for other Cost instances, too. Instead, we can define an instance of the Cost
type class for any list of type [a]
where a
has an instance for Cost
:
instance Cost a => Cost [a] where
cost = sum . map cost
Now, Haskell will automatically create an instance for Cost [Custom]
the moment we create the instance for Cost Custom
.
This looks quite similar to type constraints and this is no coincidence. We are essentially doing a type constraint, only for the entire instance of a type class. We can see this as applying the type constraint to every function declared within the type class. As such, we can again use a
's cost
function inside the Cost [a]
instance's functions.
Multiple Parameters
Standard Haskell does not allow for type classes to have more than one class parameter. However, modern compilers like ghc
support the -XMultiParamTypeClasses
compiler option that allows one to create type classes with multiple parameters.
The most obvious limitation this solves is the fact that, with single parameter type classes and the perspective of operator overloading, we can only define an add
function to add values of the same type. With multiple parameters, one could define an Add
type class that allows adding two values of different types.
class Add a b where
add :: a -> b -> a
This is not how the
add
function is implemented in standard Haskell, but this is how it could be done.
However this is still quite ugly as the return type of the addition is simply the type of the first argument, which need not always be what we want. An obvious fix to this would be adding a third parameter to the Add
type class:
class Add a b c where
add :: a -> b -> c
While this works, it poses a strange issue: One can define multiple additions for the same type that have a different output. Let's say we define the following instances for the Add
type class with three parameters:
instance Add Int Float Int where
add x y = x + roundFloatInt y
instance Add Int Float Float where
add x y = intToFloat x + y
At first this might seem fine, however when one were to write add 1 3.2
, the actual return type of such an expression is not well defined anymore and needs to be annotated explicitly for every call, e.g. add 1 3.2 :: Float
. This is not very ergonomic, thus a different more elegant solution exists.
Associated types
Associated Types are a way to define more class parameters, that are defined inside the instance of a type class and not in its signature. That means we can define Add
with three class parameters, where the third of those is fixed once the other two are.
In our Add
example, a solution using an associated type could look like this:
class Add a b where
type AddOutput a b
add :: a -> b -> AddOutput a b
We define that with any instance of Add a b
for specific types a
and b
, there shall also be a third type that's given the name AddOutput a b
. This type is then the output of our add
function. This concept ensures that there shall only be one well defined output type for an addition of two specific types a
and b
.
To define an addition for Int
and Float
, this means we have to decide what output we would even want in this case. Let's assume the choice was made to output a Float
in this case, the instance for the Add
type class would then look like this:
instance Add Int Float where
type AddOutput Int Float = Float
add x y = intToFloat x + y
One unfortunate limitation is that the AddOutput
name is not namespaced, i.e. we will need to have a different name for each associated type we have inside of different type classes.
A matrix example
Let's assume we have a data type called Matrix a
which describes a matrix whose elements are of type a
. We can then use our previously defined Add
type class to define a matrix addition for any matrices whose elements' types are addable:
instance Add a b => Add (Matrix a) (Matrix b) where
type AddOutput (Matrix a) (Matrix b) =
Matrix (AddOutput a b)
add x y = -- ...
The first line can be read as "implement Add
for two matrices whose elements' types implement Add
". The second and third lines mean "the output of such an addition shall be another matrix whose elements' type is that of the output of adding the elements of the original two matrices together".
Note that this is not how standard Haskell implements overloaded addition. Instead, Haskell has a Num
type class that defines multiple operators, such as (+)
and the unary negate, and only defines those operations on two elements of the same type. We introduced our Add
type class merely to showcase multiple parameters and associated types.