Skip to content

Commit

Permalink
Add passthrough (#274)
Browse files Browse the repository at this point in the history
  • Loading branch information
arybczak authored Nov 21, 2024
1 parent a70cf0c commit b2a416e
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 42 deletions.
5 changes: 5 additions & 0 deletions effectful-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion effectful-core/effectful-core.cabal
Original file line number Diff line number Diff line change
@@ -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
Expand Down
73 changes: 34 additions & 39 deletions effectful-core/src/Effectful/Dispatch/Dynamic.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module Effectful.Dispatch.Dynamic

-- * Sending operations to the handler
send
, passthrough

-- * Handling effects
, EffectHandler
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
-- :}
--
Expand All @@ -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
-- op1
-- op2
--
-- >>> :{
-- augmentOp2 :: (E :> es, IOE :> es) => Eff es a -> Eff es a
-- augmentOp2 = interpose_ $ \case
-- Op1 -> send Op1
-- Op2 -> liftIO (putStrLn "augmented op2") >> send Op2
-- :}
-- >>> let action = send Op1 >> send Op2 >> send Op3
--
-- >>> runEff . runE . augmentOp2 $ send Op1 >> send Op2
-- >>> runEff . runE $ action
-- op1
-- augmented op2
-- op2
-- op3
--
-- /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:
-- You can modify only specific operations and send the rest to the upstream
-- handler with 'passthrough':
--
-- >>> :{
-- 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
-- augmentOp2 :: (E :> es, IOE :> es) => Eff es a -> Eff es a
-- augmentOp2 = interpose $ \env -> \case
-- Op2 -> liftIO (putStrLn "augmented op2") >> send Op2
-- op -> send @E (coerce op)
-- op -> passthrough env op
-- :}
--
-- >>> runEff . runE . genericAugmentOp2 $ send Op1 >> send Op2
-- >>> runEff . runE . augmentOp2 $ action
-- 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
Expand Down
5 changes: 5 additions & 0 deletions effectful/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 2 additions & 2 deletions effectful/effectful.cabal
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit b2a416e

Please sign in to comment.