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
AccumTis 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
StateTwith a monoid state that we always add to. Indeed theStateTversion 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 makesAccumTdifferent 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
AccumTor the provided operations. I think the representation as aw -> m (a, w)where the inputwis the starting accumulator value, and the outputwis 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,runAccumTorrunAccum, 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
addto add stuff andlookto inspect the current value. However, the top-level user interfacerunAccumTintroduces this brand new concept of an outputwthat 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 ofAccumTabout the "meaning" of thewreturned 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
AccumTfromrunAccumTtounAccumTand modifyingrunAccumTto apply the outputwdelta to the initialw. That way, all the internals ofAccumTbehave as before, someone who really understand what they're doing can useunAccumTto build operations that operate on the delta between accumulators instead of accumulator values (e.g., creating adeltacombinator that inspects the value anAccumToperation would add to the accumulator), but casual end users whorunAccumTwill get the behavior they'd expect from an accumulator.(It's somewhat akin to the current situation with the CPS writer. Having
runWriterTreturn aw -> m (a, w)instead ofm (a, w)would be demonstrably weird, so there's anunWriterTfor those who want to work with an internal representation, and arunWriterTfor casual users who just want to use a writer in the expected way.)What distinguishes
AccumTfrom 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 twomappends 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 singlemappendfor each thing added. And they get to choose between lazy and strict monads. If they want to forbid unrestrictedputandmodify, they can put anewtypewrapper 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
AccumTwere quite inadequate. I've had a go at clarifying them.