Add passthrough (#274)

This commit is contained in:
Andrzej Rybczak 2024-11-21 20:51:32 +01:00 committed by GitHub
parent a70cf0cbf1
commit b2a416e0de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 48 additions and 43 deletions

View File

@ -1,3 +1,8 @@
# effectful-core-2.5.1.0 (????-??-??)
* Add `passthrough` to `Effectful.Dispatch.Dynamic` for passing operations to
the upstream handler within `interpose` and `impose` without having to fully
pattern match on them.
# effectful-core-2.5.0.0 (2024-10-23)
* Add `plusEff` (specialized version of `<|>`) to `Effectful.NonDet` and make
`emptyEff` and `sumEff` generate better call stacks.

View File

@ -1,7 +1,7 @@
cabal-version: 3.0
build-type: Simple
name: effectful-core
version: 2.5.0.0
version: 2.5.1.0
license: BSD-3-Clause
license-file: LICENSE
category: Control

View File

@ -20,6 +20,7 @@ module Effectful.Dispatch.Dynamic
-- * Sending operations to the handler
send
, passthrough
-- * Handling effects
, EffectHandler
@ -74,8 +75,9 @@ module Effectful.Dispatch.Dynamic
, HasCallStack
) where
import Control.Monad
import Data.Primitive.PrimArray
import GHC.Stack (HasCallStack)
import GHC.Stack
import GHC.TypeLits
import Effectful.Internal.Effect
@ -414,6 +416,25 @@ import Effectful.Internal.Utils
-- >>> runPureEff . runReader @Int 3 $ double
-- 6
-- | A variant of 'send' for passing operations to the upstream handler within
-- 'interpose' and 'impose' without having to fully pattern match on them.
passthrough
:: (HasCallStack, DispatchOf e ~ Dynamic, e :> es, e :> localEs, SharedSuffix es handlerEs)
=> LocalEnv localEs handlerEs
-> e (Eff localEs) a
-- ^ The operation.
-> Eff es a
passthrough (LocalEnv les) op = unsafeEff $ \es -> do
Handler handlerEs handler <- getEnv es
when (envStorage les /= envStorage handlerEs) $ do
error "les and handlerEs point to different Storages"
-- Prevent internal functions that rebind the effect handler from polluting
-- its call stack by freezing it. Note that functions 'interpret',
-- 'reinterpret', 'interpose' and 'impose' need to thaw it so that useful
-- stack frames from inside the effect handler continue to be added.
unEff (withFrozenCallStack handler (LocalEnv les) op) handlerEs
{-# NOINLINE passthrough #-}
----------------------------------------
-- Handling effects
@ -482,6 +503,7 @@ reinterpretWith runHandlerEs m handler = reinterpret runHandlerEs handler m
-- data E :: Effect where
-- Op1 :: E m ()
-- Op2 :: E m ()
-- Op3 :: E m ()
-- type instance DispatchOf E = Dynamic
-- :}
--
@ -490,58 +512,31 @@ reinterpretWith runHandlerEs m handler = reinterpret runHandlerEs handler m
-- runE = interpret_ $ \case
-- Op1 -> liftIO (putStrLn "op1")
-- Op2 -> liftIO (putStrLn "op2")
-- Op3 -> liftIO (putStrLn "op3")
-- :}
--
-- >>> runEff . runE $ send Op1 >> send Op2
-- >>> let action = send Op1 >> send Op2 >> send Op3
--
-- >>> runEff . runE $ action
-- op1
-- op2
-- op3
--
-- You can modify only specific operations and send the rest to the upstream
-- handler with 'passthrough':
--
-- >>> :{
-- augmentOp2 :: (E :> es, IOE :> es) => Eff es a -> Eff es a
-- augmentOp2 = interpose_ $ \case
-- Op1 -> send Op1
-- augmentOp2 = interpose $ \env -> \case
-- Op2 -> liftIO (putStrLn "augmented op2") >> send Op2
-- op -> passthrough env op
-- :}
--
-- >>> runEff . runE . augmentOp2 $ send Op1 >> send Op2
-- >>> runEff . runE . augmentOp2 $ action
-- op1
-- augmented op2
-- op2
--
-- /Note:/ when using 'interpose' to modify only specific operations of the
-- effect, your first instinct might be to match on them, then handle the rest
-- with a generic match. Unfortunately, this doesn't work out of the box:
--
-- >>> :{
-- genericAugmentOp2 :: (E :> es, IOE :> es) => Eff es a -> Eff es a
-- genericAugmentOp2 = interpose_ $ \case
-- Op2 -> liftIO (putStrLn "augmented op2") >> send Op2
-- op -> send op
-- :}
-- ...
-- ...Couldn't match type localEs with es
-- ...
--
-- This is because within the generic match, 'send' expects @Op (Eff es) a@, but
-- @op@ has a type @Op (Eff localEs) a@. If the effect in question is first
-- order (i.e. its @m@ type parameter is phantom), you can use 'coerce':
--
-- >>> import Data.Coerce
-- >>> :{
-- genericAugmentOp2 :: (E :> es, IOE :> es) => Eff es a -> Eff es a
-- genericAugmentOp2 = interpose_ $ \case
-- Op2 -> liftIO (putStrLn "augmented op2") >> send Op2
-- op -> send @E (coerce op)
-- :}
--
-- >>> runEff . runE . genericAugmentOp2 $ send Op1 >> send Op2
-- op1
-- augmented op2
-- op2
--
-- On the other hand, when dealing with higher order effects you need to pattern
-- match on each operation and unlift where necessary.
--
-- op3
interpose
:: forall e es a. (HasCallStack, DispatchOf e ~ Dynamic, e :> es)
=> EffectHandler e es

View File

@ -1,3 +1,8 @@
# effectful-2.5.1.0 (????-??-??)
* Add `passthrough` to `Effectful.Dispatch.Dynamic` for passing operations to
the upstream handler within `interpose` and `impose` without having to fully
pattern match on them.
# effectful-2.5.0.0 (2024-10-23)
* Add `plusEff` (specialized version of `<|>`) to `Effectful.NonDet` and make
`emptyEff` and `sumEff` generate better call stacks.

View File

@ -1,7 +1,7 @@
cabal-version: 3.0
build-type: Simple
name: effectful
version: 2.5.0.0
version: 2.5.1.0
license: BSD-3-Clause
license-file: LICENSE
category: Control
@ -74,7 +74,7 @@ library
, async >= 2.2.2
, bytestring >= 0.10
, directory >= 1.3.2
, effectful-core >= 2.5.0.0 && < 2.5.1.0
, effectful-core >= 2.5.1.0 && < 2.5.2.0
, process >= 1.6.9
, strict-mutable-base >= 1.1.0.0
, time >= 1.9.2