Recently, I asked a question about an instance I had created generating a runtime infinite loop, and I got a wonderful answer! Now that I understand what was going on, I have a new question: can I fix my attempt to accomplish my original goal?
Let me reiterate and clarify specifically what my problem is: I want to create a typeclass that is used to convert between some equivalent datatypes in my code. The typeclass I created is both very simple and very general: it includes a single conversion function that converts between arbitrary datatypes:
class Convert a b where
convert :: a -> b
However, this typeclass has a specific purpose: converting between a specific class of values, which have a canonical representation. Therefore, there is a specific datatype which is “canonical”, and I want to use the property of this datatype to reduce the burden on implementors of my typeclass.
data Canonical = ...
class ConvertRep a b where
convertRep :: a -> b
Specifically, consider two different representations, RepA
and RepB
. I might reasonably define some instances to convert those representations to and from Canonical
:
instance ConvertRep RepA Canonical where ...
instance ConvertRep Canonical RepA where ...
instance ConvertRep RepB Canonical where ...
instance ConvertRep Canonical RepB where ...
Now, this is immediately useful because I can now use convertRep
for both kinds of representations, but it’s mostly just serving as a way to overload the convertRep
name. I want to do something more powerful: after all, I have now effectively defined four functions with the following types:
RepA -> Canonical
Canonical -> RepA
RepB -> Canonical
Canonical -> RepB
It seems reasonable to me that, given these definitions, I should also be able to produce two functions of the following types:
RepA -> RepB
RepB -> RepA
Basically, since both datatypes can be converted to/from the canonical representation, I want to automatically produce a conversion function to/from each other directly. My attempt, as mentioned in my aforementioned question, looked like this:
instance (ConvertRep a Canonical, ConvertRep Canonical b) => ConvertRep a b where
convertRep = convertRep . (convertRep :: a -> Canonical)
Unfortunately, this instance is far too permissive, and it causes the generated code to recurse when provided two types I think should be invalid—types I have not defined canonical conversions on yet.
To try and solve this problem, I considered another, simpler approach. I figured I could use two typeclasses instead of just one to prevent this recursion problem:
class ToCanonical a where
toCanonical :: a -> Canonical
class FromCanonical a where
fromCanonical :: Canonical -> a
Now it’s possible to define a new function that performs the convertRep
conversion I was originally interested in:
convertRep :: (ToCanonical a, FromCanonical b) => a -> b
convertRep = fromCanonical . toCanonical
However, this comes at a cost of flexibility: it is no longer possible to create a direct conversion instance between two non-canonical representations.
For example, perhaps I know that RepA
and RepB
will be very frequently used interchangeably, and therefore they will be converted between each other quite a lot. Therefore, the extra step of converting to/from Canonical
is wasted time. I would like to optionally define a direct conversion instance:
instance ConvertRep RepA RepB where
convertRep = ...
which provides a “fast path” conversion between two common types.
To summarize: is there any way to accomplish all of these goals using Haskell’s type system?
- “Generate” conversions between representations given functions that convert between the representations and a canonical form.
- Optionally provide “fast paths” between instances that are commonly converted.
- Reject instances that have not been explicitly defined; that is, do not permit
ConvertRep Canonical Canonical
(and similar) instances from being created that infinitely recur and produce bottom.
The Haskell type system is very impressive, but I worry that in this case its instance resolution rules are not powerful enough to accomplish all these goals at once.