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 theStateT
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 makesAccumT
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 aw -> m (a, w)
where the inputw
is the starting accumulator value, and the outputw
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
orrunAccum
, 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 andlook
to inspect the current value. However, the top-level user interfacerunAccumT
introduces this brand new concept of an outputw
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 ismempty
, which it so often is, the values correspond. I'd be willing to bet that if you polled 100 users ofAccumT
about the "meaning" of thew
returned byrunAccumT
, 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
fromrunAccumT
tounAccumT
and modifyingrunAccumT
to apply the outputw
delta to the initialw
. That way, all the internals ofAccumT
behave as before, someone who really understand what they're doing can useunAccumT
to build operations that operate on the delta between accumulators instead of accumulator values (e.g., creating adelta
combinator that inspects the value anAccumT
operation would add to the accumulator), but casual end users whorunAccumT
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 aw -> m (a, w)
instead ofm (a, w)
would be demonstrably weird, so there's anunWriterT
for those who want to work with an internal representation, and arunWriterT
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 twomappend
s 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 fromStateT
, which has the same representationw -> m (a, w)
, but is much cheaper: a singlemappend
for each thing added. And they get to choose between lazy and strict monads. If they want to forbid unrestrictedput
andmodify
, they can put anewtype
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.