Overriding Type Class Instances

August 9, 2019

It’s well known in the Haskell world that type class instances cannot be overridden. When you have an instance in scope, you are stuck with it. Is this a feature? Is this a bug? Maybe either depending on the problem you are facing. I have my own opinions, but let me lay out the case for wanting to be able to override instances.

Disclaimer: This idea is quite experimental and not entirely fleshed out. Consider this as purely a proof-of-concept that is clearly lacking features and could potentially be refined and simplified. There’s also the chance that this has already been invented, possibly in a better form, I just have yet to come across it. That being said, you have been warned.

Consider the following very simple type class for encoding values to Text -

We’ll define some instances for it -

and they’ll do what you expect -

Now let’s say we want to derive an Encode instance for a record type -

We’ll derive Generic and use a newtype GenericEncode (that we haven’t written yet) along with DerivingVia to use the generically-derived instance for Encode.

What we’ll want is this behavior -

Going over generic derivation should be saved for another post, but I’ll quickly show the code and touch on the relevant parts.

Now this just works. Trust me, I swear.

Ok ok, this is all well and good, but let’s say we don’t like the Encode Text instance. It’s fine in most cases, but our Rec type needs to use a different instance. Do we have to give up on generic derivation and write our Encode Rec instance from scratch?

Maybe not! Let’s play with an idea.

Here’s the new Text instance we want to use. We’ll be resorting to the classic newtype trick.

Yes, it’s contrived, but you really don’t want me going into the actual use case for this. It’s much messier and this is just easier to think about.

With DerivingVia, we could possibly do something like this -

This would work, except - 1. It requires that an Encode Text instance does not already exist 2. Now everyone has to use our instance, and for our purposes we only want Rec to use it.

Maybe we can somehow embed the instance we want to override directly into the deriving clause. Hmm, how about something like -

Looks a little funny, but ignoring the clumsiness (and limitations), can this even work?

First, let’s create some types to get this thing rolling -

So we’ll use Using as a way to embed our overriding constraint into the type of the value we want to encode, and As will be used to tell the instance derivation which instance should be replaced and by which.

We can even use these types directly in the repl. Here’s the value we’ll want to be dealing with -

At this point, we can use virtually our same generic deriving machinery, just with Using sprinkled throughout to keep track of our overridden instances. I won’t copy pasta all of the boilerplate here (as it’s just uglier versions of the instances you’ve already seen). Instead, I’ll point out the most important bits.

The “entry point” instance to our generic derivation is almost exactly the same as before, except, as mentioned, with Using stuff sprinkled in everywhere. We’re actually going to rip the Using constraint bits off of the passed in type and apply it to the generic representation Rep a p, and then keep applying it on each of the field selectors so we can pick the right instance.

So aside from sprinkling in Using everywhere, we’ll need to change the instance that actually does the encoding for each field.

Now what we have is an instance that works for a given type a when we want to override it with the instance for type b. And of course, this only works if a is Coercible to b.

We can even play with this directly!

That’s actually neat on its own.

Note that we need to make this an OVERLAPPING instance so we can default to a different instance when this one doesn’t apply. Leaving it as it is would give us this when using a non-Text type -

Now we’ll define our “default” instance that works when the other doesn’t match -

There’s actually a gotcha here, but I’ll leave that as an exercise for the reader.

Moving blissfully along, now the following works -

Ok, so now let’s review our previous deriving hackery -

…but does it work?

Eureka! It does!

Is any of this practical? Maybe, maybe not. One important limitation here is that you need to write your generic deriving machinery to deal with this. Also, this implementation doesn’t deal with multiple overrides, it only supports one. However, this could likely be solved without too much effort (type-level lists are the first to come to mind.

But I think this is a good starting point. From here, we can potentially refine this approach and make it a bit more versatile. It would be pretty exciting to modify it in such a way that doesn’t require the deriving machinery to be aware of our Using and As types. I have some ideas on how to make this a reality, but there’s more experimentin’ to do first!

The actual working code for this post can be found here.