concrete functor and monad transformers

#92With non-empty initial history, `runAccumT` returns the wrong "final history"

This issue came up in a Stack Overflow question, https://stackoverflow.com/questions/76591284/how-to-use-runaccumt

Presumably, the Accum monad ought to have a single concept of the accumulated history, so if the last monadic operation is a look, the final history returned by that operation ought to be the same as the final history directly returned by runAccumT (and execAccumT). This works as expected if the initial history is mempty:

> runAccum (add 3 >> add 7 >> look) (Sum 0)
(Sum {getSum = 10},Sum {getSum = 10})

but fails with non-mempty initial history:

> runAccum (add 3 >> add 7 >> look) (Sum 999)
(Sum {getSum = 1009},Sum {getSum = 10})

Here, look returns a final history that incorporates the initial history, but the "final history" returned by runAccum ignores the initial history.

If we can get agreement that this behavior is wrong and ought to be fixed, backward compatibility be damned, then I'll be happy to prepare a patch. I imagine the right approach would be to rename the field in the newtype to a "for internal use only" unAccumT, and rewrite runAccumT appropriately.

    • description updated
  • It's hard to know how to measure right and wrong here. I think that reflects that, although AccumT is included because it's different from other monad transformers, we don't have a clear example of when to use it. Perhaps we don't have right operations either.

    If we always use the final history, that would be equivalent to a StateT with a monoid state that we always add to. Indeed the StateT version would be more efficient, as it would involve fewer calls of the monoid operation, so it's a better choice if that's what you want. What makes AccumT different is that it maintains both local and global history. But I don't think anyone has come up with a compelling application for that difference.

  • Hi, Ross. Thanks for the note.

    I'm not actually proposing changing either the representation of AccumT or the provided operations. I think the representation as a w -> m (a, w) where the input w is the starting accumulator value, and the output w is the delta between the starting and ending accumulator values is a good representation. It makes "reducing" the accumulator unrepresentable, which seems like a good thing. I just think that the top-level user interface for running an overall accumulator operation, runAccumT or runAccum, shouldn't expose this implementation detail because it's really unexpected behavior for something billed as an "accumulator".

    That is, I think the user of an "accumulator" monad will expect it to act like an abstract accumulator where you can can add things to it and inspect it's current value, but there's no real concept of an "operation" that inspects the delta between earlier and later accumulator values. This is already true for the monadic operations themselves -- you've got add to add stuff and look to inspect the current value. However, the top-level user interface runAccumT introduces this brand new concept of an output w that represents a difference between accumulator values. This is doubly confusing, because the types of this "difference" and the final accumulator value are the same, and when the initial accumulator value is mempty, which it so often is, the values correspond. I'd be willing to bet that if you polled 100 users of AccumT about the "meaning" of the w returned by runAccumT, 96 of them would say it was the final value of the accumulator. (The other four would be me and the three people who upvoted my Stack Overflow answer.)

    So, what I'm actually proposing is renaming the field in AccumT from runAccumT to unAccumT and modifying runAccumT to apply the output w delta to the initial w. That way, all the internals of AccumT behave as before, someone who really understand what they're doing can use unAccumT to build operations that operate on the delta between accumulators instead of accumulator values (e.g., creating a delta combinator that inspects the value an AccumT operation would add to the accumulator), but casual end users who runAccumT will get the behavior they'd expect from an accumulator.

    (It's somewhat akin to the current situation with the CPS writer. Having runWriterT return a w -> m (a, w) instead of m (a, w) would be demonstrably weird, so there's an unWriterT for those who want to work with an internal representation, and a runWriterT for casual users who just want to use a writer in the expected way.)

  • What distinguishes AccumT from other monad transformers that it has both: * the output added by the computation and * the input plus that output. Maintaining that distinction is expensive, requiring two mappends in each >>=. This is the essence of this monad: it's not an afterthought.

    I don't know how many people are using AccumT, but users who only want the total output would be much better off not using it. They can get exactly what they want from StateT, which has the same representation w -> m (a, w), but is much cheaper: a single mappend for each thing added. And they get to choose between lazy and strict monads. If they want to forbid unrestricted put and modify, they can put a newtype wrapper around the type with restricted operations.

    I don't think disguising this monad as what people are expecting will help anyone. It would be much better to direct such people to the monad that gives them what they want.

    I realize that the documentation doesn't make this clear. It needs extensive revision.

    • status set to closed

    I'm pretty sure we're talking paste each other here, but thanks for your time.

  • I think the real bug here was that the docs for AccumT were quite inadequate. I've had a go at clarifying them.