Haskell DerivingVia on multi param type classes with fun deps
Asked Answered
H

1

5

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.

Hombre answered 26/10, 2018 at 13:13 Comment(0)
M
6

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)
Multitude answered 26/10, 2018 at 16:13 Comment(2)
The second solution appears to needs UndecidableInstances.Hombre
Don't they both? UndecidableInstances are harmless, so I wouldn't worry about it.Multitude

© 2022 - 2024 — McMap. All rights reserved.