Down with Show! Part 1: Rules of thumb for when to use a type class

I find it enlightening that in functional programming, usually the best solution is to program with functions. ;)

Edward Kmett, from the /r/haskell thread Definitive guide on when to use typeclasses?

This is part one of three in a series of posts in which I will argue that it’s time to consign the Show type class to the dustbin of history. I won’t discuss the Show class in particular in this post, however; the purpose of this post is just to establish some background for what I’m going to propose later in the series.

The ideas in here are already fairly well-trodden ground for Haskellers; little of what I’m about to say is new. For example:

I would argue that, even though type classes are fantastically useful, it’s actually quite rare to find yourself in a position where introducing a new one is a good idea. My rules of thumb for deciding whether or not to introduce a new type class are largely inspired by Gabriel’s (see above); in my view, a type class should preferably:

Make sense as an abstraction

This means for a class C a, it should be possible to write useful functions of the form C a => whatever, and know that these functions will behave sensibly for any choice of a having a C a instance. For example, the function

(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)

which performs Kleisli composition of functions of the form a -> m b, is guaranteed to be associative for any choice of m having a Monad m instance as a consequence of the Monad laws. This means we can do things like combining a whole bunch of monadic functions with <=< without having to worry about parentheses changing the meaning of the resulting expression. The Monad class is a perfect example of this concept more generally: the Control.Monad module and the monad-loops package contain many more useful functions with Monad constraints.

Have at least one law

This is closely tied to the previous rule: it is usually the laws of a class which enable us to write code which is generic over that class and be sure that they will behave sensibly for any instance.

Have no more than a few sensible, correct behaviours for a given type (preferably just one)

If there are types which have lots of equally sensible possibilities for their instances of some class, it becomes harder to write code making use of that class; we may have to check the instances individually before knowing that they’re safe to use with a given function. In some cases, we may have to create lots of newtypes to be able to make use of functions which are generic over the class; at this point, it’s better to just use regular functions.

There are a few notable counterexamples for this one. For instance, the Monoid type class is probably one of the most justifiable type classes around, but it doesn’t meet this criterion. Most numeric types, such as Int and Double, will have at least two sensible implementations, since addition and multiplication both form a monoid.

Have some redundancy in its members’ types

I’ll illustrate what I mean by this with an example. Take fmap from Functor:

fmap :: Functor f => (a -> b) -> f a -> f b

There are three type variables f, a, and b which appear in this type signature, and all of them appear more than once (in fact, exactly twice). This helps us make the most out of the type checker. For example, suppose I wrote the following:

fmap not "hello, world"

This won’t compile, because not expects an argument of type Bool, so we can infer that in this case, the type variable a must be instantiated to Bool. However, the second argument, being a String, tells us that we will have to instantiate a to Char. We can’t do both of these together, so type-checking fails; this is good, because what we had written didn’t make sense. By contrast, consider

fromJSON :: FromJSON a => Data.Aeson.Value -> Data.Aeson.Result a

The type variable a appears only once (ignoring the constraint), which means we lose the protection afforded by redundancy. This means that it is much easier to write something which type-checks fine, but does the completely wrong thing at runtime. For instance, suppose I was parsing a JavaScript library’s package.json, and I wanted to represent the dependencies property as a Map PackageName VersionRange. Here’s an example of the dependencies property in a package.json file:

"dependencies": {
  "loose-envify": "^1.1.0",
  "object-assign": "^4.1.1",
  "prop-types": "^15.6.2",
  "scheduler": "^0.11.0"
}

But the FromJSON (Map k v) instance expects something quite different:

"dependencies": [
  ["loose-envify", "^1.1.0"],
  ["object-assign", "^4.1.1"],
  ["prop-types", "^15.6.2"],
  ["scheduler", "^0.11.0"]
]

It’s very tempting to just write parseJSON to parse one of these, but of course this won’t work, because of the above mismatch. I would argue that the FromJSON class makes it far too easy to write code which makes mistakes like this one.

Disclaimer

I’m not going to go as far as to argue that the approach I’ve described here is the unique correct way to use type classes; there are plenty of other valid options. For example, if you want to use Template Haskell to generate instances from your type definitions, such as JSON encoders and decoders, a design involving type classes like ToJSON and FromJSON is probably your best bet. Having said that, the approach I’ve detailed in this post is the one I try to follow where I can, and I’m quite confident in its effectiveness.

In the next post, I’m going to look at the Show type class with these rules of thumb in mind.

Next up: Part 2: What’s wrong with the Show type class