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
(?) :: 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

(?) :: Int -> Int -> Int
(?) x y = div (x + y) 2
(?) :: Double -> Double -> Double
(?) x y = (x + y) / 2.0

(?) :: Int -> Int -> Int
(?) 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 Int 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 itself a type. There will be no values of type Middle. Rather, a typeclass 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 generic type a:

Haskell
class Middle a where
  (?) :: a -> a -> a
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); 
}
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
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;
  }
}
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 a 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
import Data.Char

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

In GHCi, we test that the polymorphic ? operator works for all three types:

GHCi
> 6 ? 10
8
> 4.1 ? 5.2
4.65
> 'a' ? 'z'
'm'
> 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']
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

Writing a typeclass in Haskell is similar to writing an interface in Java, but Haskell developers write typeclasses far less often than Java developers write interfaces. There are two reasons.

First, what would be subclasses in Java can often be ORed together as variants in a Haskell data definition. All variants have the same type. To add an operation to the type, we write a single function that serves all variants through pattern matching. No typeclass is needed. We use typeclasses in Haskell only when we want to treat unrelated types polymorphically.

Second, Haskell ships with a good number of the useful typeclasses already written. 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"
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. As we have seen, for simple typeclasses like Show and Eq, we could automatically make them instances with a deriving clause.

← DestructuringRecord Types →