Lecture 10

The Typeclassopedia

The standard Haskell modules provide a tremendous amount of functionality, including polymorphic data structures such as arrays, maps, sets, lists, unions, etc. This power is further leveraged by the inclusion of these data structures into a small number of mathematically well-motivated typeclasses, which provide common vocabularies and interfaces to very different types. A good survey of the most important of these typeclasses can be found in the Typeclassopedia. For the next several lectures, we're going to work our way through a number of Haskell typeclasses, paying particular attention to three of the most important category-theoretically motivated typeclasses: Functor, Applicative, and Monad.

Mathematical Preliminaries: Categories

A category in mathematics is a structure that consists of a class of objects and a class of morphisms. Each morphism is associated with two (not necessarily disjoint) objects called its source and target. We write $f: A \rightarrow B$ to indicate that $f$ is a morphism with source object $A$ and target object $B$. If this notation reminds you of a declaration of a function type, that's a good thing, as “functions as morphisms” forms the basis of many important examples of categories, but there are other kinds of categories whose morphisms are not functions at all. Categories also come with a partial operation (called composition, and usually denoted by $\circ$) on morphisms, such that whenever $f : A \rightarrow B$ and $g : B \rightarrow C$, then $g \circ f : A \rightarrow C$, together with an identity morphism $1_A$ for every object $A$, and satisfies the following axioms:

Note that there's often more than one name for various category-theoretic objects. E.g., the words arrow and map are synonyms for morphism, domain is a synonym for source, and range is a synonym for target. These different nomenclatures reflect the generality of category theory.

The Hask category is an important example for us, whose objects are grounded Haskell types, whose morphisms are Haskell functions with the corresponding source and target types, in which $\circ$ is (.), and in which the identity morphisms $1_A$ are just the polymorphic Haskell identity function id, grounded at the corresponding type A.

Note here that Hask has a lot of structure that is not fully captured by the notion of a category, e.g., consider the set $\hbox{hom}(A,B) = \{f \;|\; f : A \rightarrow B \}$, the set of functions of type $A \rightarrow B$. We can think of $\hbox{hom}(A,B)$ both as a particular collection of morphisms within Hask, but also as the object $A \rightarrow B$ within Hask. This dual identity of $\hbox{hom}(A,B)$ both as a set of arrows and as an object of Hask is a consequence of the fact that Haskell handles multi-adic functions via currying, and implies a much richer structure than your basic category.

Type-Theoretic Preliminaries: the Kinds of Types

We're comfortable thinking of (Int,Int) as a type. Indeed, it is a grounded type, of which we'll have more to say later. But what then is (,) itself? It is tempting to think of (,) as a kind of function that takes types as arguments, and returns a new type. And it's one of those rare things: a good temptation. Indeed, if we reflect on it, we can see that Haskell's type system incorporates many of the notions of the λ-calculus, including abstraction, application, and currying.

Let's take this bit by bit. Consider the type [(String,Int)], which could arise in practice as a way to represent a function from String to Int. If we wanted to abstract over the result type Int, we can do so by introducing a new type abbreviation:

type Dict a = [(String,a)]

Thus, a Dict can be thought of as a list of objects of some type, indexed by strings. We can now re-introduce our original type [(String,Int)] by applying Dict to Int, like this:

type IntDict = Dict Int

Less obviously, we could have taken the inner part of the Dict definition, and abstracted it as

type StringAssoc a = (String,a)

re-expressed the right-hand side in terms of the prefix (,)

type StringAssoc a = (,) String a

and then, wait for it..., η-reduced the type definition to

type StringAssoc = (,) String

Yes, Haskell really does let us do this. Having delved this deeply into madness, you won't be surprised to know that we can define

type Compose g f a = g (f a)

and then use Compose to re-express the definition of Dict, which sets us up to η-reduce the result:

type Dict a = Compose [] ((,) String) a type Dict = Compose [] ((,) String)

The question is why? We'll get there. But before we do, let's consider the sort of question that we began this section with: What exactly is Dict, which we think of as describing a function from types to types? Haskell's answer is that Dict is a type, as is Dict Int. The essential distinction between Dict and Dict Int is in their kind, which we can think of as the type of their type. Fortunately, the kind system is easy. All ground types have kind *. Types like Dict, which we think of as being a function from types to types, has kind * -> *, while (,) has kind * -> * -> *. In Haskell, only ground types, types of kind * are inhabited, i.e., have values. But types of higher kind are important because they can participate in Haskell's typeclass system, as we'll soon see.

Functors

Functors arise as homomorphisms of categories, i.e., functions that take the objects and morphisms of a category, and map them to a possibly different category in such a way as to preserve identity arrows and compositions. In Haskell, Functor is a typeclass whose members f have kind * -> *, and which defines a function fmap that maps functions of type a -> b to functions of type f a -> f b so that they preserve identities and compositions:

In code,

class Functor f where fmap :: (a -> b) -> f a -> f b

satisfying

fmap id = id fmap (f . g) = fmap f . fmap g

The instances of Functor defined in the Prelude and elsewhere in the standard Haskell modules all satisfy this law, and yours should too.