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
(?) :: 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
(?) :: 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:
-
Use a different symbol for the
Intdefinition. -
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:
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:
public interface Middle<T> {
T middle(T that);
}
public interface Middle<T> {
T middle(T that);
}Unlike the Haskell functions, there's only one formal parameter. In an object-oriented language like Java, the first operand of a method call 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
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;
}
}
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:
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:
> 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:
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:
-
The
Eqtypeclass imposes the==and/=operations on any type whose values may be compared for equality. -
The
Showtypeclass imposes theshowfunction on any type whose values can be turned into strings. -
The
Readtypeclass imposes thereadfunction on any type whose values can be parsed from a string. -
The
Ordtypeclass imposes thecompare,min, andmaxfunctions and the inequality operators on any type whose values can be ordered. -
The
Numtypeclass imposes+,-,*, and several other functions on any type whose values can be treated numerically. -
The
Fractionaltypeclass imposes/on any type whose values support real number division. -
The
Integraltypeclass imposesdivandmodon 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"
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.