Typeclasses

Dear Computer

Chapter 9: Types Revisited

Typeclasses

Suppose we want to define a new infix operator ? that computes the average of two floats. For example, 5.3 ? 5.9 yields 5.6. We define a function using the standard named function syntax:

Haskell
(?) :: Double -> Double -> Double
(?) x y = (x + y) / 2.0

Since the name is made up of punctuation rather than an identifier, it acts as an operator and must be enclosed in parentheses.

Later we find that we also need to compute the average of two integers. We attempt to write a second definition of ? that accepts integers:

Haskell
(?) :: Double -> Double -> Double
(?) x y = (x + y) / 2.0

(?) :: Integer -> Integer -> Integer
(?) x y = div (x + y) 2

But this code fails to compile because Haskell doesn't normally allow overloading. That leaves us with two choices:

  1. Use a different symbol for the Integer definition.
  2. Define a typeclass that imposes the ? operation on any participating type.

Option 1 is silly. Imagine if we had to write 5 +++ 6 to add integers because + was reserved for floats. Option 2 is the better choice. Let's define a typeclass.

Defining a Typeclass

Let's call the typeclass Middle. A typeclass is not a type. Rather, it is used to impose constraints on types, much as an interface in Java imposes constraints on the classes that implement it. The Middle typeclass imposes operation ? on type a:

Haskell
class Middle a where
  (?) :: a -> a -> a

As we've seen in function type signatures, a is a generic placeholder for some actual type. The ? function is given a type signature but not a definition. It accepts two parameters of the generic type a and yields an a. A typeclass may include default definitions if they can be written generically. In this case, ? does slightly different things for floats and integers, so no default definition is provided.

An equivalent Java interface looks like this:

Java
public interface Middle<T> {
  T middle(T that); 
}

The analogy isn't perfect because Java is object-oriented. Only the second operand is listed in the formals. The first operand is the implicit receiver this.

Making Instances

With the typeclass in place, we now define ? once for floats and once for integers using the instance command:

Haskell
instance Middle Double where
  (?) x y = (x + y) / 2.0

instance Middle Integer where
  (?) x y = div (x + y) 2

These instance declarations provide real types for the generic a. There's no mention of Middle a. Instead we have Middle Double and Middle Integer.

This equivalent Java code declares the Double class as an implementation of Middle and defines the middle method:

Java
public class Double implements Middle<Double> {
  public Double middle(Double that) {
    return (this.value + that.value) * 0.5;
  }
}

Bringing new types into the Haskell typeclass later on requires only an additional instance declaration and definition of the ? function for the new type. Here the Char class is made an instance of Middle:

Haskell
import Data.Char

instance Middle Char where
  (?) x y = chr $ div (ord x + ord y) 2

In GHCI, we test that the universal ? operator works for all three types:

GHCI
> 6 ? 10
8
> 4.1 ? 5.2
4.65
> 'a' ? 'z'
'm'

There. Now Haskell has overloading—thanks to typeclasses.

Constraining

Once we have a typeclass, we may write polymorphic functions that target the abstract interface rather than a specific type. A polymorphic function's signature must include a generic type, upon which we impose a typeclass constraint using the => syntax that we have already seen. For example, this sandwich function accepts two parameters of any type that is an instance of Middle:

Haskell
sandwich :: Middle a => a -> a -> [a]
sandwich lo hi = [lo, lo ? hi, hi]

sandwich 6 10     -- yields [6, 8, 10]
sandwich 4.1 5.2  -- yields [4.1, 4.65, 5.2]
sandwich 'a' 'z'  -- yields ['a', 'm', 'z']

Builtin Typeclasses

Haskell ships with enough useful typeclasses that casual Haskell developers may find that they rarely need to write their own. The work of abstraction has been done by our forebears. Here is a subset of the builtin typeclasses that we may target in our polymorphic functions:

When we define our own types, we may wish to make them members of these builtin typeclasses so that we can pass values of our type to routines like sort from the standard library. We do this by declaring our custom type to be an instance of the typeclass and defining the required functions, as is done here to make Direction an instance of Show:

Haskell
data Direction = North | South | East | West

instance Show Direction where
  show North = "N"
  show South = "S"
  show East = "E"
  show West = "W"

The definition of show is broken up into four subdefinitions using pattern matching.

← DestructuringRecord Types →