Reputation: 10740
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
Upvotes: 2
Views: 375
Reputation: 80744
Short answer: you need a functional dependency on your class.
Long answer:
When the compiler sees run
, it needs to find the appropriate instance of Runnable
in order to determine which implementation of run
to use. And in order to find that instance, it needs to know what a
and b
are. It knows that a
is a ReaderT
, so that one is covered. But what is b
?
The compiler sees that you're using b
as a function, passing ()
to it as argument. Therefore, thinks the compiler, b
must be of shape () -> t
, where t
is not known yet.
And here is where it kinda stops: the compiler has nowhere to get t
from, therefore it doesn't know b
, therefore it can't find the appropriate instance, so kaboom!
But there is a remedy for this situation. If we look closer at what your Runnable
class actually means, it's easy to see that b
should be strictly defined by a
. That is, if we know what the monad is, we know what the return value would be. Therefore, it should be possible for the compiler to determine b
by knowing a
. But alas, the compiler doesn't know that!
But there is a way to explain that to the compiler. It's called "functional dependency", and it's written like this:
class Runnable a b | a -> b where
This notation a -> b
tells the compiler that b
should be unambiguously determined by a
. This will mean that, on one hand, the compiler won't let you define instances that violate this rule, and on the other hand, it will be able to find the appropriate instance of Runnable
just by knowing a
, and then determine b
from that instance.
Upvotes: 6
Reputation: 29193
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.
Upvotes: 5