Reputation: 2223
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
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