Functional# Typeclasses in Haskell (and Elm?)

6 min read

·

By Øyvind Stette Haarberg

·

December 16, 2020

Typeclasses are a way to allow functions to take constraints on the type of arguments, allowing for ad hoc polymorphic functions (we say ad hoc as the function for the concrete type is provided when invoked). They were originally introduced as a way to overload arithmetic and equality operators in Haskell, but they have many other uses as well. Interfaces are a similar concept in object-oriented languages.

You can use the `+`

function to add both integers and floats, so the type of + clearly can't be `Int -> Int -> Int`

or `Float -> Float -> Float`

. We can't insert arbitrary types (`[3] + [4]`

is an error for instance, by trying to add two lists), so the type of `+`

must be more restrictive than `a -> a -> a`

. The type of `+`

is actually

`(+) :: Num a => a -> a -> a`

where `Num a =>`

denotes a typeclass constraint on the type a, requiring to be an instance of the Num typeclass.

The powerful part of defining functions on constrained types is that it is extensible, and we can implement a typeclass if we want to be able to use these functions for our custom types. Let's say we are modeling an analog clock in our program, and we have a type for time:

`data AnalogTime = Time Int Int -- hours and minutes`

we can then provide an instance for the `Eq`

typeclass (the name `Eq`

derives from *equality*), which allows us to compare types using `(==)`

for equal and `(/=)`

for not equal:

```
instance Eq AnalogTime where
(Time hoursA minutesA) == (Time hoursB minutesB) = hoursA == hoursB && minutesA == minutesB
```

Note that by just implementing `(==)`

, we can also use the `(/=)`

operator for AnalogTime values. Now that AnalogTime has an instance for the `Eq`

typeclass, we can also provide an instance for the `Ord`

typeclass. The `Ord`

typeclass has functions for types with an *ordering*, or in other words types that can be compared with for example `>`

or `<`

. This is because all `Ord`

instances are required to have an `Eq`

instance, as we can see from the definition:

```
class Eq a => Ord a where
(<=) :: a -> a -> Bool
-- other functions omitted
```

As we saw with the `Eq`

instance we didn't have to provide all functions for the functions of the typeclass. This benefit might not seem to be significant when looking at `Eq`

, but for other typeclasses like `Foldable`

you get a whole load of functions already defined if you implement a sufficient part of the typeclass functions.

If we look at the info for the `Ord`

typeclass we can see that the minimal definition is providing `(<=)`

. Just by providing a function for comparing lesser than or equal, we gain the other comparisons, as well as the `max`

and `min`

functions for free. Additionally, we are guaranteed that the definitions for our typeclass are *coherent* this way. In other words, the definitions have to internally make sense, such that we can't have that `a > b && b > a`

for instance.

In Haskell we can define custom typeclasses, and also provide instances for any custom type as a typeclass. In the Elm programming language we cannot define custom typeclasses, and you are also unable to provide instances for the existing typeclasses. There are some typeclasses in Elm, such as `number`

, `comparable`

and `appendable`

, which allow for functions such as `(+)`

, `(>=)`

and `(++)`

to take in a variety of built-in types. If you want to write functions that can work for several different types as long as they have some function defined, the functions you need have to be an explicit part of the function's type.

```
type Ordering = LT | EQ | GT
listSort : (a -> a -> Ordering) -> List a -> List b
```

The Elm definition looks similar to a Haskell definition using typeclasses:

`listSort :: Ord a => [a] -> [a]`

This way you don't have the added abstraction of providing a mapping to some other type. It is easier to use the `Ord`

typeclass still, as you have more functions out of the box such as `min`

that can be used.

A difference between the Haskell and Elm examples is that for Haskell the correct function for comparing the elements will be sourced by the `Ord`

instance for a (whichever type a turns out to be), but for the Elm examples you have to provide the correct instance when calling the function.

The need to know the types is easier to see if you look at the `map`

-functions in the Elm standard library. Instead of having a single map-function for some typeclass (`Functor`

for instance), Elm provides different mapping functions for the standard containers (such as `List.map`

, `Dict.map`

, `Maybe.map`

and so on). This way you don't need typeclasses for `Functors`

, but there is no way to map over some structure in Elm without also knowing *what* that structure is. This is a tighter coupling to the types used. If the underlying type changes in your model, say from a `List`

to a `Maybe`

, then any functions simply mapping over the elements now have to provide the appropriate mapping function.

In some ways you are able to do a similar thing to typeclasses in Elm by passing around records containing the necessary definitions for a typeclass:

```
-- Elm custom typeclass
type alias Ord a =
{ lessThan : a -> a -> Bool
, greaterThan : a -> a -> Bool
, equal : a -> a -> Bool
}
```

And we can now use this to again define our list sort:

`listSort : Ord a -> List a -> List a`

Not too far off from our Haskell `listSort`

!

You still can't extend the built-in typeclasses and the typeclass functions aren't provided implicitly, but you can write functions using a constrained type as you know that the caller has to provide the instance for your typeclass. We also lose the guarantee of *coherence* for the typeclass functions. Even if you wrote a helper function that fills the rest of the instances from a minimal definition, we could still provide an `Ord`

record where the `lessThan`

- and `greaterThan`

-functions are the same.

One thing that breaks with this approach is typeclasses for higher-kinded polymorphism. Trying to use a record to implement a similar set of functions as `Foldable`

in Haskell (the typeclass of types that can be "folded over", a more general version of `reduce`

for instance) illustrates this issue.

It is sufficient to only define `foldr`

for a `Foldable`

instance, similar to `(<=)`

for `Ord`

.

`foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b`

We can see from the definition of `foldr`

that we have a type `t`

that operates in a similar way to `List`

(as an example) where the type isn't just `List`

but `List`

of `a`

. If we try the same approach as for `Ord`

we get a syntax error on the `t a`

part of the function definition, as this is not supported in Elm.

If you want to try typeclasses like these in Elm there is a package with several custom typeclasses implemented, by the way of using type aliases for records.