fused-effects / fused-effects

A fast, flexible, fused effect system for Haskell

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Accum effect mixed with other effects duplicates accumulated monoidal value

byorgey opened this issue · comments

Here is a minimal example that demonstrates the problem:

import Control.Carrier.Accum.Strict
import Control.Carrier.Lift
import Data.Monoid (Sum (..))

addOne :: (Has (Accum (Sum Integer)) sig m, Has (Lift IO) sig m) => m ()
addOne = do
    add (Sum (1 :: Integer))
    sendIO $ putStrLn "hi"

main :: IO ()
main = do
    s <- runM . execAccum (Sum (0 :: Integer)) $ addOne
    print (getSum s)

Since we simply run addOne once, which adds 1 to the accumulated value (which we start at 0), this should print 1. However, it prints 2. If we add a second call to sendIO $ putStrLn "hi", it prints 4 (after printing hi twice, of course). It seems like every additional bind causes the accumulated state to be combined with itself.

  • Control.Carrier.Accum.Church and Control.Carrier.Accum.Strict both seem to have this issue, whereas Control.Carrier.Accum.IORef works as expected.
  • The problem only seems to manifest when there is another effect mixed in besides the Accum effect. This perhaps suggests that the issue is in the Algebra (Accum w :+: sig) (AccumC w m) instance, though I don't understand what's going on there well enough to be sure.

Perhaps this line

R other -> thread (uncurry runAccum ~<~ hdl) other (w, ctx)

should have (mempty, ctx) instead of (w, ctx)? Yielding (w, ctx) would be like implementing a state monad, where the state is passed through unchanged. But as far as I understand that is not how this carrier is implemented. A function w -> (w,a) represents an Accum computation by taking the state of the accumulator at the beginning of the computation, and returning only any additional values produced during the computation, not the total accumulation. So a computation which contains no Add effects should ignore the input w and return mempty.

I think this would explain why running other effects causes the accumulated state to be duplicated.

Edit: I can confirm that making that change seems to resolve the issue, though I am not certain that it doesn't break something else. I would be happy to prepare a PR if that would be helpful.

@patrickt any thoughts? No rush, just a friendly bump in case you haven't seen this.

Perhaps this line

fused-effects/src/Control/Carrier/Accum/Strict.hs

Line 135 in 77b51e9

R other -> thread (uncurry runAccum < hdl) other (w, ctx)
should have (mempty, ctx) instead of (w, ctx)? Yielding (w, ctx) would be like implementing a state monad, where the state is passed through unchanged. But as far as I understand that is not how this carrier is implemented. A function w -> (w,a) represents an Accum computation by taking the state of the accumulator at the beginning of the computation, and returning only any additional values produced during the computation, not the total accumulation. So a computation which contains no Add effects should ignore the input w and return mempty.

I think this would explain why running other effects causes the accumulated state to be duplicated.

Edit: I can confirm that making that change seems to resolve the issue, though I am not certain that it doesn't break something else. I would be happy to prepare a PR if that would be helpful.

Sorry for the delay getting back to you about this. Great catch both with the behaviour and this diagnosis; I think you hit the nail on the head. A PR would be delightful if you're still interested; I'd particularly like to see if we can capture the correct behaviour in terms of laws like we've done with most of the other effects, as that enables us to automatically test against multiple carriers, and paves the way for testing stacks with multiple effects in play (as, IIRC, we do with NonDet).

No worries! I will definitely put together a PR, at the very least with a simple bug fix, but I will also give some thought to laws and putting together some tests.