Search code examples
haskellderivingderivingvia

Haskell DerivingVia on multi param type classes with fun deps


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.


Solution

  • 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)