jakubdaniel
jakubdaniel

Reputation: 2223

Resolving overlap of instances with type families

I have a class

class Monad m => MyClass m where
    type MyType1 m
    type MyType2 m
    ...

    a :: m (MyType1 m)
    b :: m (MyType2 m)
    c :: m (MyType3 m)
    ...

and I also have bunch of instances that implement the functions (a ... ) reasonably

instance MyClass A where
    type MyType1 A = Int
    ...

    a = ...
    ...

instance MyClass (B a) where
    type MyType1 (B a) = Char
    ...

    a = ...
    ...

...

but I also have a lot of instances that don't do anything useful apart from lifting the implementation through transformers:

 instance MyClass m => MyClass (MyTransA m)
    type MyType1 (MyTransA m) = MyType1 m
    ...

    a = lift a
    ...

 instance MyClass m => MyClass (MyTransB m)
    type MyType1 (MyTransB m) = MyType1 m
    ...

    a = lift a
    ...

 ...

and this turns out to be a lot of boilerplate to write, so I wanted to replace these repetitive uninteresting instances with simply

class MonadTrans t => AutoLiftMyClass t

instance (AutoLiftMyClass a, MyClass m) => MyClass (a m)
    type MyType1 (a m) = MyType1 m
    ...

    a = lift a
    ...

which would allow me to write only

instance AutoLiftMyClass MyTransA
instance AutoLiftMyClass MyTransB
...

to get the lifting for free, avoiding the enumeration of all the lifted a, b, ... for every MyTransA, MyTransB, ...

The problem is that for whatever reason (I really don't know why) GHC considers only the RHS of instance declarations, thus my AutoLiftMyClass collides on type family instances for MyType1, ... with all the reasonable instances A, B, ... (those that don't declare an instance of AutoLiftMyClass)

I have seen some posts and articles on wiki about closed type families but they do not make much sense to me. Is there any way how to make this idea work?

Upvotes: 0

Views: 375

Answers (1)

user2407038
user2407038

Reputation: 14578

You could use DefaultSignatures, which is supposed to solve exactly this problem:

class Monad m => MyClass m where

  type MyType m :: * 
  type MyType m = MyTypeDef m 

  val :: m (MyType m)
  default val :: (MyClassDef m) => m (MyTypeDef m) 
  val = defVal 

The variants of MyClass, MyType are just a copy of the above, essentially:

class MyClassDef m where 
  type MyTypeDef m :: * 
  defVal :: m (MyTypeDef m)

instance 
  (MonadTrans t, Monad n, MyClass n
  ) => MyClassDef (t (n :: * -> *)) where 
  type MyTypeDef (t n) = MyType n 
  defVal = lift val 

Note that an instance is only really needed to cleanly pattern match on the t n constructor. Overlap isn't an issue, because this will only be used in default signatures.

Then your instances are simply:

instance (MyClass m) => MyClass (ReaderT r m)  
instance (MyClass m, Monoid r) => MyClass (WriterT r m)  
instance (MyClass m) => MyClass (StateT r m)  

Of course it may be desirable to have multiple options for a default implementation, but this isn't much harder than the above - you simply add another type to the class:

class MyClassDef (ix :: Symbol) m where 
  type MyTypeDef ix m :: * 
  defVal :: m (MyTypeDef ix m)

instance 
  (MonadTrans t, Monad n, MyClass n
  ) => MyClassDef "Monad Transformer" (t (n :: * -> *)) where 
  type MyTypeDef "Monad Transformer" (t n) = MyType n 
  defVal = lift val 

Note that ix is ambiguous in defVal, but I'll use TypeApplications to get around it. You can accomplish the same with Proxy.

The additional parameter is determined when you write the instance, and assuming you don't use overlapping instances (which you shouldn't if you want good type inference, esp. if you want type inference to work with other mtl-style libraries) you can just add it as an associated type:

class Monad m => MyClass m where
  type UseDef m :: Symbol 

  type MyType m :: * 
  type MyType m = MyTypeDef (UseDef m) m 

  val :: m (MyType m)
  default val :: (MyClassDef (UseDef m) m) => m (MyTypeDef (UseDef m) m) 
  val = defVal @(UseDef m) 

If you forget to implement UseDef, you will get an error like:

* Could not deduce (MyClassDef (UseDef (StateT r m)) (StateT r m))
    arising from a use of `Main.$dmval'

but you can provide your own custom error for a missing default if you want:

instance (TypeError (Text ("No default selected"))) => MyClassDef "" m

class Monad m => MyClass m where
  type UseDef m :: Symbol 
  type UseDef m = ""

and if you implement all the methods and types, you get no error, as UseDef isn't used anywhere - only in an instantiated default signature, which won't even exist if the implementation is given.

Your instances incur the cost of an additional line of boilerplate, but it isn't much (esp. with copy-paste):

instance (MyClass m) => MyClass (ReaderT r m) where 
  type UseDef (ReaderT r m) = "Monad Transformer" 

instance (MyClass m, Monoid r) => MyClass (WriterT r m) where 
  type UseDef (WriterT r m) = "Monad Transformer" 

instance (MyClass m) => MyClass (StateT r m) where 
  type UseDef (StateT r m) = "Monad Transformer" 

Note that you do have to supply the required contexts for each instance.


Note that all this is really necessary if you only care about avoiding overlapping instances. If you don't, then use the simple solution and just write

instance {-# OVERLAPS #-} (AutoLiftMyClass a, MyClass m) => MyClass (a m)

or turn on OverlappingInstances.

Upvotes: 2

Related Questions