James Burton
James Burton

Reputation: 794

Is it possible to define a symmetric typeclass?

Consider the following typeclass for defining equality between values of heterogenous types:

class HEq a b where
    heq :: a -> b -> Bool

We can define an instance for Int and Bool as follows:

instance HEq Int Bool where
    heq 1 True = True
    heq 0 False = True
    heq _ _ = False

But if HEq Int Bool then we can also say that HEq Bool Int. Is there a way to capture this relationship without having to define two instances?

A naive approach would be the following:

instance HEq a b => HEq b a where
  heq = flip heq

But this requires UndecidableInstances and needs to be annotated as OVERLAPPABLE to even satisfy the compiler. And then we enter infinite recursion territory on anything that doesn't have an explicit instance defined since it just flips back and forth indefinitely.

Upvotes: 3

Views: 104

Answers (2)

amalloy
amalloy

Reputation: 92117

This is a little bit less automated than Daniel Wagner's answer, but may be satisfactory as it doesn't get in the way of your existing default instance.

An obvious first step towards a solution (with some drawbacks) would be to define a newtype wrapper to tell the compiler that you want to use the flipped equality rules:

newtype EqH a = EqH a

instance HEq b a => HEq a (EqH b) where
  heq a (EqH b) = heq b a

Now you never need to write your instance HEq Bool Int, because the client can always call heq True (EqH 1) and get the newtype version. Great, the implementation is easy, but now the callsites suck: they have to know whether to call heq x y or heq x (EqH y) based on which version of HEq is defined, and even if they could keep track of which to use it's a nuisance to write the EqH wrapper.

You can make the callsites perfect by doing a little bit more work in the implementation. Delegating to a typeclass instance through a newtype is a very common pattern, and it's what DerivingVia is for. So if, in addition to the above newtype, you add a deriving instance for each pair of types you give an HEq instance to, clients can call heq with the arguments swapped, no problem:

instance HEq Int Bool where
  heq 1 True = True
  heq 0 False = True
  heq _ _ = False

deriving via (EqH Int) instance HEq Bool Int

ghci> heq (1 :: Int) True
True
ghci> heq True (1 :: Int)
True

So in the end it's not free: you do have to write one instance declaration per instance you want to define. But the deriving instance is very easy to write, taking much less work than defining the original instance and not requiring you to rewrite the code of the flip instance either.

Upvotes: 4

Daniel Wagner
Daniel Wagner

Reputation: 153172

You can't write a blanket HEq a b => HEq b a instance. But you could add a default implementation to the class declaration.

{-# Language DefaultSignatures #-}
class HEq a b where
    heq :: a -> b -> Bool

    default heq :: HEq b a => a -> b -> Bool
    heq = flip heq

With that in place, you can write your flipped instances with no body.

instance HEq Bool Int
instance HEq Int Bool where
    heq = ...

instance HEq () Char
instance HEq Char () where
    heq = ...

Upvotes: 5

Related Questions