The One Function per Typeclass Rule

After about five years programming in Haskell, I think we need a rule:  Only put one function in a typeclass.

Why?  Because inevitably someone comes along with a data type for which one or the other function of a typeclass is perfectly suited, and yet another function of the same typeclass is not implementable.

Here are some examples of consequences of breaking the rule:

  1. The Infamous Set Monad, which requires splitting Monad in half. (Monad)
  2. All of the abstract ways to construct Nothing: fail (Monad), mempty (Monoid), mzero (MonadPlus), empty (Alternative).  Not surprisingly, all of these typeclasses are subtly related.
  3. The natural numbers, which have a minBound, but not a maxBound (Bounded) . . .
  4. . . . and which support addition and multiplication, but aren’t closed under subtraction and for which the concept of a sign does not exist (Num).
  5. My own memoizable message type, which would like to implement Applicative, but needs a monadic computation to implement pure. (Applicative)

It’s a little extra typing to write multiple “class . . . where” clauses for each type that needs to implement a large number of type-indexed functions, but it’s quite easy to combine related typeclasses when appropriate, as follows:

class Foo a where
    foo :: a -> b

class Bar a where
    bar :: a -> b

class (Foo a,Bar a) => FooBar a where
    {this space intentionally left blank}

In conclusion, you should definitely follow this rule if I have convinced you that it is a good idea to follow it.

5 thoughts on “The One Function per Typeclass Rule

  1. There’s at least one big exception to this being a good rule: classes with somehow mutually definable methods. For instance, Ord

    class Ord a where
    compare :: a -> a -> Ordering
    ( a -> Bool

    It actually has a lot of methods, and sensibly so, because any one of several works as the default (although, the defaulting facilities could be buffed as well: let me specify all possible defaults, with implementations in terms of each, instead of working out an implicit graph with one or two possible nodes like now).

    Another I want is:

    class ... => Monad m where
    (>>=) :: m a -> (a -> m b) -> m b
    join :: m (m a) -> m a

    I should be able to pick which I want to define. Or both if I want. And allowing me to override some more specific combinators isn’t a bad idea either, like the new Functor:

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

    Applicative and Monad are likely to have similar possible, “this could be significantly more efficient for some implementations,” methods. People seem averse to this sort of thing (and often suggest RULEs instead, which are unportable), but I don’t really see the problem with indulging a bit (you can obviously go overboard).

    As an additional aside: I’m not sure we can blame the ‘various kinds of failure’ mess on multiple methods specifically. It’s a combination of factors.

    1. fail exists for dubious reasons that don’t have to do with a separate class. Failure was specifically intended to be possible for every Monad in H98.
    2. Alternative exists separately from MonadPlus because the latter predates Applicative, but there’s no reason to require all MonadPlusses to be a Monad. So it’s a hierarchical (and legacy code) issue.
    3. Monoid exists apart from the others (at least) because you cannot write a constraint like (forall a. Monoid (m a)). So it’s a type system issue.

    Anyhow, I don’t see anything wrong with one-function-per-class as a basic guideline, but I wouldn’t want it to be enforced by the language for instance (like it used to be in Clean).

  2. Another thing you have to be quite careful about is the coherency of the laws of a type class. For example, I’ve recently been arguing that it doesn’t make too much sense to split up an asynchronous exception type class into three components, because the semantics of exceptions don’t really make sense unless you have all the components.

    With constraint kinds, one-function-per-class should become more pleasant to work with, however.

  3. Whilst this kind of rule sounds nice, it is over-simplistic.

    Type-classes typically seem to serve two purposes:

    * Define a general property of many types (Eq, Functor, Monad, etc.)

    * Defining an API for using common types interchangeably (analogous to ML-style Functors if I understand them correctly).

    This rule completely fails this second case: the whole point of the type-class is to *have* that large grouping of methods together. Admittedly this second usage of type-classes isn’t used as much in Haskell, but there are still cases.

  4. I was thinking about that, and I ddceied to explicitly write out the if then else, because it showed use of mzero/return, rather than a sort of magical (to those who don’t quite understand its operation) guard operator. I guess it would be a good thing to mention, though.

  5. I’ve been learning Haskell in the last two mhnots, and even though I love the brevity (a-la Python) and type safety (a-la C++) of Haskell, I find myself disappointed with Haskell’s speed I wrote a simple Mandelbrot calculation in different ways (list comprehensions, tail recursion), and found it to be MUCH slower than the corresponding C/C++ code. The final blow was when I realized that I didn’t have to use my own complex type and could use the library’s Complex only to see the speed get even slower! Also disappointing is that being new to Haskell (but very experienced in the imperative world x86 ASM, C++, Python, etc) I can’t seem to trace the reasons behind the speed differences (since the actual code doing the work is actually very little, there is obviously a lot of hidden code that is generated from GHC). I invite you for a simple brainstorming (mandelbrot’s calculations are very simple) at ttsiodras at the well known gmail dot com , so we can have a look together and see what’s going on

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s