Is there a way to declare a generic parametrized type Conf
that offers/provides a function frames
depending on the type parameter d
, like
{-# LANGUAGE GeneralizedNewtypeDeriving
, MultiParamTypeClasses
, FunctionalDependencies #-}
import Control.Applicative
import Control.Monad
import Control.Monad.Identity
import Control.Monad.Trans.Class
import Control.Monad.Trans.State
import Control.Monad.Trans.Except
data MyConf d = MyConf { frames :: [d] } -- my parametric type
-- define a class to expose the interface in monads
class ConfM d m | m -> d where
getFrames :: m [d]
-- wrap StateT to include MyConf and allow instance of ConfM
newtype MyStateT d m a = MyStateT { runMyStateT :: StateT (MyConf d) m a }
deriving (Functor, Applicative, Monad, MonadTrans)
-- expose the interface over selected transformers
instance Monad m => ConfM d (MyStateT d m) where
getFrames = MyStateT $ fmap frames get
instance (ConfM d m, Monad m) => ConfM d (ExceptT a m) where
getFrames = lift getFrames
so that then one can write something like:
-- hide the gory implementation details
type MyMonad d = ExceptT A (MyStateT d B) C
class SomeClass a
-- this is the desired goal:
-- to concisely write an 'algorithm' in MyMonad only once
-- but for all possible choices of d from SomeClass
example :: SomeClass d => MyMonad d
example = do
fs <- getFrames
-- do SomeClass stuff with d
return ()
-- assume Int is instance of SomeClass
instance SomeClass Int
-- give me an instance of the above generic 'algorithm'
exampleInt :: MyMonad Int
exampleInt = example
-- assuming for example
type A = ()
type B = Identity
type C = ()
In the above code I am stuck at:
test.hs:23:25:
Illegal instance declaration for ‘ConfM d (MyStateT d m)’
(All instance types must be of the form (T a1 ... an)
where a1 ... an are *distinct type variables*,
and each type variable appears at most once in the instance head.
Use FlexibleInstances if you want to disable this.)
In the instance declaration for ‘ConfM d (MyStateT d m)’
test.hs:26:38:
Illegal instance declaration for ‘ConfM d (ExceptT a m)’
(All instance types must be of the form (T a1 ... an)
where a1 ... an are *distinct type variables*,
and each type variable appears at most once in the instance head.
Use FlexibleInstances if you want to disable this.)
In the instance declaration for ‘ConfM d (ExceptT a m)’
With additional FlexibleInstances
test.hs:27:14:
Illegal instance declaration for ‘ConfM d (ExceptT
The coverage condition fails in class ‘ConfM’
for functional dependency: ‘m -> d’
Reason: lhs type ‘ExceptT a m’ does not determine
Using UndecidableInstances might help
In the instance declaration for ‘ConfM d (ExceptT a
So I need UndecidableInstances
.
Your question seems a bit vague, but it sounds like a potential use case for a multi-parameter typeclass with a functional dependency.
{-# LANGUAGE MultiParamTypeClasses
, FunctionalDependencies #-}
class Monad m => MyClass d m | m -> d where
getDs :: m [d]
Then MyClass d m
means that m
is a Monad
and that getDs
can be used to produce a value of type m [d]
. The purpose of the functional dependency is to indicate that m
determines d
. There is exactly one instance declaration of MyClass
for each m
, and that class must determine what d
is. So you could write an instance like
instance MyClass Int IO where ...
(which would then be the only one allowed for IO
), but not something wishy-washy like
instance MyClass d IO where ...
because the latter does not determine d
.
You might find my choice of argument order for MyClass
a bit strange. There is some method to this madness. The main reason for it is that MyClass
can be partially applied. Partially applying it to m
isn't too useful, because that leaves a constraint that can only be satisfied by one possible argument. Partially applying it to d
, on the other hand, could be useful, because there could be multiple choices of m
for a given choice of d
. Thus given {-# LANGUAGE ConstraintKinds #-}
,
type MakesInts = MyClass Int
is a potentially useful constraint. I believe using this order may also help avoid the need for UndecidableInstances
in some cases, but I'm not certain.
An alternative others mentioned is to use an associated type family.
{-# LANGUAGE TypeFamilies #-}
class Monad m => MyClass m where
type Available m
getDs :: m [Available m]
This does essentially the same thing, but
Anyone writing an instance of MyClass
must include a line like, e.g., type Available IO = Int
.
Anyone placing a constraint on the Available
type will need to use Available
in the constraint, and will need FlexibleContexts
(not a big deal).
The type family offers access to the associated type.
Type families are expressed in GHC Core (AKA System FC), so they're better-behaved than functional dependencies in some regards.
1 (especially) and 2 are arguably downsides of the type family approach; 3 and 4 are upsides. This largely comes down to a question of taste.