A Month of Haskell, Day 7 - Type Classes

Posted on May 11, 2017 by Chris Lumens in .

I’ve mentioned type classes on day 2, day 5, day 6, and day 7. I didn’t intend to aim this series at people just starting with Haskell, but type classes are such a fundamental concept and so central to the language that I need to write about them before I go too much farther.

The name “type class” is kind of unfortunate. It doesn’t really have much to do with object oriented programming. Haskell doesn’t have classes or objects. The term “instance” comes up when working with type class, but instances don’t package up values or contain any state. Type classes are really just a way of declaring that a set of operations are supported by a given type.

However, there are passing similarities. Type classes are how Haskell achieves operator overloading. Type classes can inherit from other type classes, and there’s even multiple inheritance.

Defining a type class

Here comes some of that OO-like terminology. Let’s just look at one of the simplest type classes to see how they are defined. The Eq class is defined like so:

class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool
    x /= y = not (x == y)

This type class defines two functions: == and /=. It also provides a default definition for the /= function. You’ll notice that it’s defined using ==, which is not given a definition. So, one of these functions has to be defined for real or this will never work. The documentation mentions a minimal complete definition. This means that when a type is made an instance of this class, it must provide a real definition of either == or /=.

Any type that is an Eq can be compared with these two operators, without having to use special type-specific functions. That’s how it is like operator overloading. However, unlike operator overloading in other languages I think it avoids adding a lot of obscure magic behind-the-scenes stuff to the language.

Instantiating a type class

When you make a type an instance of a class (yet more OO terminology), you are saying that it provides at least the minimal complete definition. The type supports all the operations in that list. The Haskell documentation lists all the types that are an instance of a given class, either where the class is documented or where the type is documented.

Because every built-in type is an instance of the Eq class and the code that creates those instances is hiding deep in ghc internals, we’ll have to make our own type to demonstrate.

data Rec = Rec { name :: String,
                 rank :: String,
                 serialNumber :: Int }

instance Eq Rec where
    a == b = (name a) == (name b) &&
             (rank a) == (rank b) &&
             (serialNumber a) == (serialNumber b) 

I’ve defined a cheesy record type with a couple members. Making it an instance of Eq is really quite straightforward. I have provided a definition of the == function, and this can be used to make /= work. Other types may require more complex comparison functions, or they could be quite simple. It all depends on what behavior you want.

Automatic deriving

In the previous example, I gave a definition of == manually. However, ghc is really smart and can create the instances for lots of type classes automatically. Because we just comparing every item in the == function, we can tell ghc to do automatic deriving like so:

data Rec = Rec { name :: String,
                 rank :: String,
                 serialNumber :: Int }

Multiple type classes can be listed as a comma-separated list. ghc can derive lots of instances for you, while others require using language extensions. Talking about that is beyond the scope of this post. In general, I like to use the automatic deriving feature for all the basic classes. There’s no point in writing code if you don’t have to.

Class constraints

Look at type signatures in the documentation long enough, and you’ll come across something like this:

elem :: Eq a => a -> t a -> Bool

In fact, these have already come up plenty of times in previous posts where I’ve given the signatures of newly introduced functions.

This simply means that every a in the type signature must be an instance of Eq. Instead of being completely generic and being able to take any type, it is constrained by what’s on the left side of the =>. You may need to add constraints to your own functions if the code is not completely generic and requires the parameters to support specific functions.

It is possible to have constraints on several types in the signature, and it’s possible to have multiple constraints on the same type. The syntax is the same:

mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)
stateFileName :: (Functor m, MonadIO m) => m FilePath

The open world

If you’re writing a library you expect other people to use, there’s one thing to be aware of - anyone using a type from your module can make it an instance of any class they want. As a library author, you don’t have any control over this. It’s called the open world. It also means you can take any type from anyone else’s module and make it an instance of any class you want, including classes you define. This can be handy if you need specialized behavior out of stuff in the Prelude.

Type class inheritance

This is an advanced topic that I’ve never had to do, but it’s worth being aware of. When I talked about the Alternative type class, I mentioned that an Alternative is just an Applicative with some extra functions. That should sound like inheritance to you, and in fact type classes support this with some extra syntax. Here’s how Alternative is defined:

class Applicative f => Alternative f where
    empty :: f a
    (<|>) :: f a -> f a -> f a

Applicative is the base type class, and the => is the extra little piece of syntax to separate the base class from the subclass. Instances of Alternative therefore support whatever functions are in the definition of Applicative, plus whatever’s in Alternative. This is used a fair bit in the base library - an Applicative is a subclass of Functor, which means that both Applicative and Alternative instances also support whatever is in the Functor definition.

Multiple inheritance

I’m going to spend even less time on multiple inheritance, because I haven’t had to use it yet and I’d prefer it if I never did. It comes up in the base library fairly often, and you may encounter code that does it so you should at least be aware of the syntax. A type class can inherit from multiple other type classes with an obvious extension of the above syntax. Here’s an example from the base library:

class (Alternative m, Monad m) => MonadPlus m where
   mzero :: m a
   mzero = empty

   mplus :: m a -> m a -> m a
   mplus = (<|>)

Useful type classes

The reason I say type classes are fundamental to the language is that there’s so many of them, and they provide so many of the most basic functions in the language. Without them, there’d be no way to compare types, do arithmetic, build lists, do IO, or handle errors.

I intend to add a much more complete writeup about a lot of these in the future, but just to give you an idea of how pervasive type classes are, here’s a quick run down of some major ones: