Running effects locally
re-xyr opened this issue · 13 comments
While other effect libraries, no matter which approach, uses an effect stack that may change in different parts of programs, capability
encourages the use of one single concrete monad to handle all effects. This is good for performance, however also means that there is no effective way to "run" an effect anyhow, except unwrapping the whole monad.
Oftentimes, a user (i.e. me) has a function simple
that may raise an exception, but it is catched and handled in another function, say complex
, so complex
by itself won't raise any exception.
In a conventional library like freer-simple
, I could write:
simple :: (Member (Error MyErr) m) => Int -> Eff m ()
complex :: Int -> Eff m ()
complex n = do
...
result <- runError $ simple n
case result of
Left e -> ...
Right x -> ...
But there is no similar functionality to runError
in capability
because it has a fixed monad. If simple
has HasThrow ...
then complex
must have HasThrow
too, which does not model the behavior correctly.
Is there any existing idiom tackling this problem? If not, could we have a typeclass that simulates "running effects" on a fixed monad locally?
Hello,
We have the Capabiliy.Reflection module to handle this use case (see the linked examples).
Please close this issue if this answers your question satisfactorily.
Alternatively, in the specific case, you could also use Capability.Derive
with an existing strategy such as MonadError
on ExceptT
. E.g. runError
could be expressed as
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
import Capability.Derive
import Capability.Error
import qualified Control.Monad.Except as MTL
runError :: Monad m => (forall m'. HasThrow "foo" Foo m' => m' a) -> m (Either Foo a)
runError m = MTL.runExceptT $ derive @MonadError @'[HasThrow "foo" Foo] @'[] $ m
Will this kind of functions (along with runReader
, runState
etc) make their way into the library?
I'm not sure that it would be so useful. As you can kind of see in the type that @aherrmann is proposing, capabilities from the outer monad are not transmitted through a monad transformer.
You could use lift
and a type like this:
runError :: Monad m => (forall t. (MonadTrans t, HasThrow "foo" Foo (t m)) => t m a) -> m (Either Foo a)
runError m = MTL.runExceptT $ derive @MonadError @'[HasThrow "foo" Foo] @'[] $ m
Or maybe we coud specialise t
to ExceptT
.
But to reiterate, this would not work if applied to a function which has type a (HasThrow "foo" Foo m, HasReader "bar" Bar m)
constraint. You would really need the function to have require cabailities of the form (HasThrow "foo" Foo (t m), HasReader "bar" Bar m)
. Which is quite different.
The strength of algebraic effect libraries is that effects commute with handlers this way. In exchange of restrictions on effects. The capability library is more lightweight in that it really lets you use any type class as effects, but what it loses is a good way to handle effect via monad transformers like this.
To expand on lift a little. Capability has a Lift
combinator that can be used to lift capabilities through MonadTrans
instances. I had a hard time coming up with the right type signature for runError
to make this generic. But, inlining it into complex
yields something that works:
complex :: (Monad m, HasReader "bar" Int m) => m String
complex = do
result <-
MTL.runExceptT $
derive @Lift @'[HasReader "bar" Int] @'[MTL.MonadError Foo] $
derive @MonadError @'[HasThrow "foo" Foo] @'[HasReader "bar" Int] $
simple
case result of
Left e -> pure $ "Error: " ++ show e
Right () -> pure "Succeeded"
simple :: (HasThrow "foo" Foo m, HasReader "bar" Int m) => m ()
The outer derive
lifts HasReader
through ExceptT
and forwards MTL.MonadError
to make it available to the inner derive
.
The inner derive
then provides HasThrow
via MonadError
and forwards HasReader
.
Thanks for the answers! Seems that one big problem is having to lift capabilities through transformers. But in ReaderT
-pattern, I think if we use MonadUnliftIO
to handle exceptions via try
, instead of ExceptT
, we won’t need to do any lifting; everything stays in one monad. Is that a favorable idea? (I don’t know much about the performance of try
vs pure ExceptT
.)
I just realized that would involve adding MonadUnliftIO m
to every function signature. Maybe it’s not a good idea...
to handle exceptions via
try
, instead ofExceptT
, we won’t need to do any lifting; everything stays in one monad. Is that a favorable idea?
You can use the HasCatch
for this particular pattern. And this is indeed easier in the style advertised by the capability library. Though @aherrmann has shown that, contrary to what I was saying, we can go the ExceptT
route.
What I thought was that if we have HasCatch
, then we necessarily have HasThrow
as implied in the class signature, while in fact, complex
doesn't throw. This means that we imprecisely specified the effects. It wasn't very devastating though - we're only expressing an effect that we don't actually use, instead of not expressing what we use, which could be more serious a problem.
The reasoning behind why HasCatch
is a sublcass of HasThrow
is that you can't catch an exception if it hasn't been thrown. Since, in the methods of HasCatch
there is a single monad, this monad must be able to throw errors of corresponding type, otherwise catch
doesn't make much sense.
What if we have the catch
signature as (HasThrow e m => m a) -> (e -> m a) -> m a
?
Thank you for all the answers, closing this issue. (PS I started to write an effect library inspired by capability
)
What if we have the
catch
signature as(HasThrow e m => m a) -> (e -> m a) -> m a
?
Because of the coherence assumption in GHC, this is not much different of a type.
PS I started to write an effect library inspired by
capability
Best of luck with it!