How can I write a generic function run
that takes an object of some monad transformer, and calls the corresponding function?
Given run s
,
s
is a StateT
, run = runStateT
s
is a ReaderT
, run = runReaderT
s
is a MaybeT
, run = runMaybeT
I've tried creating a typeclass Runnable
:
:set -XMultiParamTypeClasses
:set -XFlexibleInstances
class Runnable a b where
run :: a -> b
(//) :: a -> b
(//) = run
instance Runnable (StateT s m a) (s -> m (a, s)) where
run = runStateT
instance Runnable (ReaderT r m a) (r -> m a) where
run = runReaderT
But when I try using run
, it doesn't work. For example, let's define simpleReader
, which just returns 10
when read:
simpleReader = ReaderT $ \env -> Just 10
runReaderT simpleReader ()
This outputs Just 10
, like expected.
However, when I try using run
, it gives me an error:
run simpleReader ()
<interactive>:1:1: error:
• Non type-variable argument in the constraint: Runnable (ReaderT r Maybe a) (() -> t)
(Use FlexibleContexts to permit this)
• When checking the inferred type
it :: forall r a t. (Runnable (ReaderT r Maybe a) (() -> t), Num a) => t
If I enable FlexibleContexts
like it suggests, I get a different error:
<interactive>:1:1: error:
• Could not deduce (Runnable (ReaderT r0 Maybe a0) (() -> t))
(maybe you haven't applied a function to enough arguments?)
from the context: (Runnable (ReaderT r Maybe a) (() -> t), Num a)
bound by the inferred type for ‘it’:
forall r a t. (Runnable (ReaderT r Maybe a) (() -> t), Num a) => t
at <interactive>:1:1-19
The type variables ‘r0’, ‘a0’ are ambiguous
• In the ambiguity check for the inferred type for ‘it’
To defer the ambiguity check to use sites, enable AllowAmbiguousTypes
When checking the inferred type
it :: forall r a t. (Runnable (ReaderT r Maybe a) (() -> t), Num a) => t
The output type of run
is totally determined by the input type to it. Represent this as a functional dependency (-XFunctionalDependencies
):
class Runnable a b | a -> b where
run :: a -> b
-- side note: (//) does not belong here
(//) :: Runnable a b => a -> b
(//) = run
-- instances as given
Now it works. The reason your version doesn't work is because there's no way to know what the output type should be. If you have e.g.
act :: ReaderT Env Identity Ret
Then
run act :: Runnable (ReaderT Env Identity Ret) b => b
And then we're stuck, there's no way to figure out what b
should be. It's e.g. possible to add another instance
instance Runnable (ReaderT r m a) (ReaderT r m a) where
run = id
And now run act
can be either a ReaderT Env Identity Ret
or Env -> Identity Ret
. The dependency a -> b
a) allows us to infer b
from a
and b) restricts instance
declarations to make this possible. Conflicting instances like the one I gave are rejected, and run act
has the b
type inferred to Env -> Identity Ret
by looking at the instance
you gave, as desired.