ekmett / machines

Networks of composable stream transducers
Other
339 stars 46 forks source link

pass and listen in PlanT's MonadWriter instance are probably invalid #95

Open ivan-timokhin opened 6 years ago

ivan-timokhin commented 6 years ago

This evaluates to [1]:

testListen :: [Int]
testListen =
  execWriter . runT_ . construct $ do
    (_, w) <- listen $ tell [1]
    tell w

while the same function sans runT . construct (i.e. working directly on WriterT) evaluates to [1,1].

Likewise, with pass, this evaluates to [1]:

testPass :: [Int]
testPass =
  execWriter . runT_ . construct . pass $ do
    tell [1]
    pure ((), const [])

while the WriterT version evaluates to [].

I've grouped them in a single issue since the cause is very similar. In the definitions of listen and pass (with some manual inlining)

  listen m = PlanT $ \kp ke kr kf -> runPlanT m (\x -> kp =<< listen (return x)) ke kr kf

  pass m = PlanT $ \kp ke kr kf -> runPlanT m (\x -> kp =<< pass (return x)) ke kr kf

both listen and pass from the base monad are applied to return x, which has mempty as its accumulated writer state. So listen now always returns mempty, and pass only operates on mempty, leaving actual state accumulated in the provided action unchanged.

YoEight commented 6 years ago

Thanks for reporting this. Don't have much time ATM to fix it (I wrote that instance years ago). Did you find out a fix in the meantime?

ivan-timokhin commented 6 years ago

Best I can do is to steal the idea from similar monad in Control.Monad.Trans.Free.Church (free package). There, pass and local both internally convert the argument to a non-CPS representation, work on it, and then convert back (local does something much more interesting, which I don't understand yet).

The idea, then, is to introduce a non-CPS version of PlanT, implement pass and listen for it, and then use it internally in PlanT versions.