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:
(?) :: 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:
(?) :: 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:
-
Use a different symbol for the
Integer
definition. -
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
:
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:
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:
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:
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
:
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:
> 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
:
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:
-
The
Eq
typeclass imposes the==
and/=
operations on any type whose values may be compared for equality. -
The
Show
typeclass imposes theshow
function on any type whose values can be turned into strings. -
The
Read
typeclass imposes theread
function on any type whose values can be parsed from a string. -
The
Ord
typeclass imposes thecompare
,min
, andmax
functions and the inequality operators on any type whose values can be ordered. -
The
Num
typeclass imposes+
,-
,*
, and several other functions on any type whose values can be treated numerically. -
The
Fractional
typeclass imposes/
on any type whose values support real number division. -
The
Integral
typeclass imposesdiv
on any type whose values support integer division.
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
:
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.