Classy Type Classes

Characteristics

Scope:
Module
Safety :
Has none. Don't use. Just stay away!

Explanation

This pattern is guaranteed to blow on your face.

Or, better, if used with care, with frozen requirements, and without shaking the codebase with any too strong refactoring, it may resist long enough to not only blow in your face, but also get you by surprise and ensure maximal cost on fixing the damage.

That said, let's get to it!

Often one gets a set of class instances that do not benefit from the default implementations of the class, but have very similar implementations between themselves. When that happens, it seems useful to create another type class, more easy to implement for those instances, and then instantiate the desired class in terms of the newly created one.

That sounds confusing, thus let's get an example. And for greatest evil potential, let's choose an actually useful example. And, why stop there? Let's get an example just stable enough to not blow before you are fully commited to it!

Let's say you have a bunch of enums:

data Enum1 = A1 | B1 | C1 deriving (Eq, Ord, Bounded, Enum)
data Enum2 = A2 | B2 | C2 deriving (Eq, Ord, Bounded, Enum)

Now, let's also say you have some types similar to this:

data PolyEnum = PolyA EnumA | PolyB EnumB deriving (Eq, Ord)

It is easy to see how this type could be an enum, yet, you can't just derive bounded or enum for it. You could go on and manually write your instances, but if you have many of those types, this gets very repetitive. Alternatively, you can create another class, and define the instances for this class:

class EnumeratedEnum h where
  allValues :: [h]

instance EnumeratedEnum h => Bounded h where
  minBound = head allValues
  maxBound = last allValues

instance EnumeratedEnum h => Enum h where
  toEnum i = allValues !! i
  fromEnum e = fst . head . filter (\x -> snd x == e) $ zip [0..] allValues

And then, you just instantiate your type as:

allBounded = [minBoud..maxBound]
instance EnumeratedEnum PolyEnum where
  allValues = map PolyA allBounded ++ map PolyB allBounded

And that's it! You have an Enum...

Except that if you try, you'll notice the above code does not compile. GHC will complain about one or two... dozens of overlapping instances.

The way to "fix" this vary from case to case. For this example, you can get it to compile by adding an {-# OVERLAPPABLE #-} pragma on the Bounded and Enum instances of EnnumeratedEnum.

Except that now GHC will start complaining about orphan instances!

If you made through the drill, you'll probably know that mixing overlapping and orphan instances is a big no-no... Well, congratulations, you've not only just mixed them, but did it in a single declaration!

If you want to proceed anyway, you can make the warning go away by adding {-# OPTIONS_GHC -fno-warn-orphans #-} to the top of your file. Add enough indirection and you may even be able to make your coworkers file a bug against GHC instead of pointing fingers at you when it finally blows.

Well, if you do really feel like doing that, you'll also need FlexibleInstances and UndecidableInstances for this example to work. And, congratulations! You just buried a mine on your codebase ready to blow on the face of any unwarned bystander. Have fun watching!

So... What do I do?

All problems aside, the code above is useful. Or, well, it would be if it worked.

The question is, how do I solve the same problem in a way that is safe? And, to the best of my knowledge, the answer is metaprograming. Use TemplateHaskell. Yes, it's harder to write, but it will actually solve the problem, instead of creating a bigger one.