Type Classes
If you have programmed in Java before, you are hopefully familiar with interfaces. If you have programmed in Rust or Go before, you should be familiar with traits. In Java, Rust or Go, we would say that a type implements an interface or trait. In Haskell, we say that a type is an instance of a type class. That's merely a change in terminology. The concept is exactly the same in all of these languages.
Let's look at interfaces in Java. An interface specifies a list of methods. To implement such an interface, a class needs to provide implementations for all the methods in the interface. Similarly, a trait in Go or Rust specifies a list of functions. To implement this trait for some type, we need to provide implementations of all functions in the trait for this type.
In Haskell, we define a type class by defining a bunch of type signatures of functions that types in this type class need to support. That's the same as specifying a bunch of methods in an interface without providing implementations for them. To make a type an instance of this type class, we need to provide implementations of all of these functions for this type.
Let's use some examples to motivate and develop two commonly used type classes
in Haskell. Assume we want to implement the elem
function that reports whether
a given list contains a given element. This function does in fact exist as part
of the standard library, but we'll develop it from scratch. If we ask whether
the list [1, 2, 3]
contains the element 2
, the answer is yes (True
):
>>> 2 `elem` [1,2,3]
True
The element 4
is not in this list:
>>> 4 `elem` [1,2,3]
False
If we wanted the elem
function to work only for lists of Int
s, here is how
we would implement it:
elem :: Int -> [Int] -> Bool
elem q = containsQ
where
containsQ [] = False
containsQ (x:xs) | x == q = True
| otherwise = containsQ xs
This says that that the query element q
is not contained in the empty list. If
the list is non-empty, that is, it is of the form x:xs
, then if x == q
, then
q
is clearly part of the list. If x /= q
, then x:xs
contains q
if and
only if xs
contains q
, so we call containsQ xs
recursively.
Now, implementing an elem
function only for integer lists is silly. Clearly,
our implementation does not use the fact that the list elements are integers at
all. The only property of the list elements that the function relies on is that
we can test whether any given list element equals q
. So, we would like to
implement a much more widely applicable version of elem
that allows us to test
whether any list of type [a]
, a list of a
s, contains a particular value of
type a
. Here's what this would look like:
elem :: a -> [a] -> Bool
elem q = containsQ
where
containsQ [] = False
containsQ (x:xs) | x == q = True
| otherwise = containsQ xs
The problem is that this doesn't work:
>>> :{
| elem :: a -> [a] -> Bool
| elem q = containsQ
| where
| containsQ [] = False
| containsQ (x:xs) | x == q = True
| | otherwise = containsQ xs
| :}
<interactive>:66:26: error:
• No instance for (Eq a) arising from a use of ‘==’
Possible fix:
add (Eq a) to the context of
the type signature for:
elem :: forall a. a -> [a] -> Bool
• In the expression: x == q
In a stmt of a pattern guard for
an equation for ‘containsQ’:
x == q
In an equation for ‘containsQ’:
containsQ (x : xs)
| x == q = True
| otherwise = containsQ xs
Since a
can be any type, Haskell cannot possibly guess when we want to
consider two elements of type a
to be equal. It is our responsibility to
implement an appropriate equality test for values of type a
. We can group all
types that support equality tests together to form a class of types, the type
class Eq
. Our elem
function will work for any type a
that is a member of
this type class or, in Haskell speak, is an instance of this type class.
That's what the error message above tells us, once we know how to read it. The
current type signature elem :: a -> [a] -> Bool
claims that elem
should work
for any type; no constraints whatsoever. The compiler looks at the
implementation of elem
and realizes that on the second-last line, we use
x == q
as a pattern guard. It concludes that a
cannot just be any type: it
must support equality tests, it must be an instance of Eq
. The error message
"No instance for (Eq a)
arising from the use of ==
" says that according to
our type signature, the type a
is not required to be an instance of Eq
, but
the use of the ==
operator in the expression x == q
requires a
to be an
instance of Eq
. Thus, we have a problem. The error message even says how we
can get rid of the error: "Possible fix: add (Eq a)
to the context of the type
signature [...]." So let's do that:
elem :: Eq a => a -> [a] -> Bool
elem q = containsQ
where
containsQ [] = False
containsQ (x:xs) | x == q = True
| otherwise = containsQ xs
Now our function compiles:
>>> :{
| elem :: Eq a => a -> [a] -> Bool
| elem q = containsQ
| where
| containsQ [] = False
| containsQ (x:xs) | x == q = True
| | otherwise = containsQ xs
| :}
We'll talk about the syntax for imposing constraints on the argument types of functions shortly. For now, let's look at how we define type classes.