I'm trying to use DerivingVia
to cut the boilerplate on instance definitions for a multi parameter type class with functional dependencies.
I have these types and class:
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE DerivingVia #-}
newtype Wrapper t = Wrapper t
newtype Wrapper2 t = Wrapper2 t
class MyEq a f | a -> f where
eq :: a -> a -> f Bool
-- Instance for 'Wrapper2'
instance Eq t => MyEq (Wrapper2 t) Wrapper2 where
eq (Wrapper2 t) (Wrapper2 t') = Wrapper2 (t == t')
I want to derive MyEq (Wrapper Int) Wrapper
using deriving via
.
My first attempt was to use:
deriving via Wrapper2 instance MyEq (Wrapper Int) Wrapper
As discussed in the paper section 6.2, https://www.kosmikus.org/DerivingVia/deriving-via-paper.pdf, this looks for a MyEq (Wrapper Int) Wrapper2
instance, the second argument was "changed" but the first one is still Wrapper Int
.
Obviously instance MyEq (Wrapper Int) Wrapper2
does not exists because I implemented instance MyEq (Wrapper2 Int) Wrapper2
.
I cannot "cheat" by creating (see Wrapper
as first type argument):
-- Instance for 'Wrapper2'
instance Eq t => MyEq (Wrapper t) Wrapper2 where
eq (Wrapper2 t) (Wrapper2 t') = Wrapper2 (t == t')
Because in this case the functional dependency Wrapper t -> Wrapper2
is not respected.
I can easily solve the issue by rewriting eq :: f a -> f a -> f Bool
and removing the functional dependency, but I'd like to avoid changing this API.
So first of all, let's repeat that the instance you want to be derived for you is this one:
instance MyEq (Wrapper Int) Wrapper where
eq (Wrapper t) (Wrapper t') = Wrapper (t == t')
I cannot see a way to derive the class in exactly the way you want, because as you observe yourself, this requires you to change both class parameters, but we can currently only derive through the last.
One possibility is to flip the class arguments, so that the "important" class parameter (the one that determines the other) becomes the last, and then tweak the wrapper type you derive via to include some helpful information, like this:
class MyEq f a | a -> f where
aeq :: a -> a -> f Bool
Function aeq
retains the same type, but the class arguments of MyEq
are flipped.
Now Wrapper2
gets an extra parameter to let us specify the desired value of f
when deriving:
newtype Wrapper2 (f :: Type -> Type) t = Wrapper2 t
Now the instance for Wrapper2
can be defined without explicitly specifying f
:
instance (Eq t, Coercible Bool (f Bool)) => MyEq f (Wrapper2 f t) where
eq (Wrapper2 t) (Wrapper2 t') = coerce (t == t')
The extra parameter in Wrapper2
is necessary here to satisfy the functional dependency.
Now we can derive the desired instance as follows:
deriving via Wrapper2 Wrapper Int instance MyEq Wrapper (Wrapper Int)
This works because now, GHC is looking for
an instance MyEq Wrapper (Wrapper2 Wrapper Int)
, and this matches the one we have
provided.
You can achieve the same using an associated type:
class MyEq a where
type Result a :: Type -> Type
eq :: a -> a -> Result a Bool
Same definition of Wrapper2
with the extra argument. The instance becomes
instance (Eq t, Coercible Bool (f Bool)) => MyEq (Wrapper2 f t) where
type Result (Wrapper2 f t) = f
eq (Wrapper2) (Wrapper2 t') = coerce (t == t')
deriving via Wrapper2 Wrapper Int instance MyEq (Wrapper Int)