lexi-lambda / freer-simple

A friendly effect system for Haskell
https://hackage.haskell.org/package/freer-simple
BSD 3-Clause "New" or "Revised" License
227 stars 19 forks source link

Handling the same effect multiple times with interpret and interpose #38

Open koslambrou opened 2 years ago

koslambrou commented 2 years ago

Here's self-containing example:

data MyEffect r where
    Execute :: MyEffect ()

makeEffect ''MyEffect

handleFirstEffect :: (LastMember IO effs) => MyEffect ~> Eff effs
handleFirstEffect = \case
    Execute -> liftIO $ print "handleFirstEffect"

handleSecondEffect :: (LastMember IO effs) => MyEffect ~> Eff effs
handleSecondEffect = \case
    Execute -> liftIO $ print "handleSecondEffect"

main :: IO ()
main = do
  runM
     $ interpret handleSecondEffect
     $ interpose handleFirstEffect execute

The programs prints only handleFirstSecond, but does not print handleSecondEffect. I would have expected to also print the latter, since interpose should allow someone to respond to the effect while leaving it unhandled.

Bug or is there's something I'm not understanding?

lexi-lambda commented 2 years ago

You’re right that the documentation for interpose is somewhat confusing/ambiguous. Really, interpose does intercept and “retire” each action executed by send, so enclosing handlers do not see the intercepted actions. If you want that behavior, you need to explicitly re-send the action in the interposing handler:

handleFirstEffect :: (LastMember IO effs, Member MyEffect effs) => MyEffect ~> Eff effs
handleFirstEffect = \case
  Execute -> do
    liftIO $ print "handleFirstEffect"
    send Execute

This is generally a Good Thing, because it gives you more control. For example, if you wanted "handleSecondEffect" to be printed first, you could swap the order of the statements:

handleFirstEffect :: (LastMember IO effs, Member MyEffect effs) => MyEffect ~> Eff effs
handleFirstEffect = \case
  Execute -> do
    send Execute
    liftIO $ print "handleFirstEffect"

The benefit of using interpose over interpret is simply that interpose does not eliminate the effect from the effs type-level list and therefore does not require it to be the first element of that list. This is convenient when you do not actually want to grow the list of effects, you just want to add a more-nested handler for an effect you know is already present. Otherwise, interpose doesn’t do anything special, it works just like interpret.

Does that answer your question? (I’ll leave this issue open regardless, since the documentation should be clarified.)

koslambrou commented 2 years ago

Thank you very much. It does answer my question :)