tweag/capability

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...

@aherrmann

But, inlining it into complex yields something that works:

This is great 🙂

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?

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!