2

I am trying to replace StateT with AccumT in my program but I can't figure out exactly how it works. Specifically, I don't understand why runAccumT and its derivatives seem to ignore their second argument. For example, I was expecting

execAccumT (add 7) 8 :: IO Int

to print 15, but it prints 7. What am I missing here?

Ron Inbar
  • 2,044
  • 1
  • 16
  • 26

2 Answers2

4

Updated with a workaround.

First, note that Accum uses a general Monoid, not just a sum, so even though your code happens to work because it involves only one monadic add, anything that actually involved multiple accumulator operations would fail to type check:

> execAccumT (add 7 >> add 8) 0 :: IO Int
<interactive>:39:19-20: error:
    • No instance for (Monoid Int) arising from a use of ‘>>’
    ...

Instead, as in @DanielWagner's answer, you need to use the Sum monoid:

> execAccumT (add 7 >> add 8) 0 :: IO (Sum Int)
Sum {getSum = 15}

You can continue to write add 7 instead of add (Sum 7) because Sum Int has a Num instance that automatically converts numeric literatals to sums, and allows simple arithmetic operations on them, but if you wanted to add x where x :: Int, you'd need to write add (Sum x) explicitly.

Second, there does appear to be a bug in the handling of the initial value of the accumulator, what the documentation calls its "initial history". If you write:

> execAccumT (add 7 >> add 8) 999 :: IO (Sum Int)

it appears that the initial history is ignored, but if you use runAccumT to simultaneously inspect both what look thinks is the final history, and the final history returned by runAccumT, you can see a discrepancy:

λ> runAccumT (add 7 >> add 8 >> look) 999 :: IO (Sum Int, Sum Int)
(Sum {getSum = 1014},Sum {getSum = 15})

That is, look's opinion of the final history is that it includes the initial history, but the final history returned by runAccumT doesn't include the initial history.

For now, it's safe to use Accum with an "mempty" initial history, i.e., whatever is the appropriate zero value for the Monoid, and use an initial add operation if you want to start with something other than mempty.

If you want to use a non-mempty history, I suggest importing with a few functions hidden and use these replacements:

import Data.Functor.Identity
import Control.Monad.Trans.Accum hiding (runAccum, execAccum, runAccumT, execAccumT)

runAccumT :: (Functor m, Monoid w) => AccumT w m a -> w -> m (a,w)
runAccumT (AccumT f) w = (\(a, w') -> (a, w <> w')) <$> f w

execAccumT :: (Monad m, Monoid w) => AccumT w m a -> w -> m w
execAccumT m = fmap snd . runAccumT m

runAccum :: (Monoid w) => Accum w a -> w -> (a, w)
runAccum m = runIdentity . runAccumT m

execAccum :: (Monoid w) => Accum w a -> w -> w
execAccum m = snd . runAccum m

These should work fine:

λ> runAccumT (add 7 >> add 8 >> look) 999 :: IO (Sum Int, Sum Int)
(Sum {getSum = 1014},Sum {getSum = 1014})
K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • 1
    I submitted [issue #92](https://hub.darcs.net/ross/transformers/issue/92). – K. A. Buhr Jun 30 '23 at 20:08
  • 1
    Probably the right fix is simply not to expose the extra `w` argument in the API, i.e. have `runAccumT act = realRunAccumT act mempty` etc. It seems like this implementation strategy doesn't make much sense otherwise, the discrepancy between "input accumulation" and "output accumulation" is just too weird. – Daniel Wagner Jun 30 '23 at 20:08
  • 1
    That was my first thought, but I think it can be fixed up at the back end. Right now, `runAccumT` just unwraps the `w -> (a, w)` newtype, but it actually needs to apply that operation to the initial history and *then* `mappend` the initial and returned histories to get the final history. – K. A. Buhr Jun 30 '23 at 20:11
  • Ah, nice. Yeah, that would probably fix everything up. – Daniel Wagner Jun 30 '23 at 20:13
  • With the help of GitHub Copilot I managed to roll [my own `AccumT`](https://github.com/roninbar/nim/blob/accum/src/Main/Trans/Accum.hs) which works as expected and also has `instance (Monad m, Monoid w) => MonadAccum w (AccumT w m)` for every monad `m` rather than just for `Identity`. ` – Ron Inbar Jul 01 '23 at 18:29
  • 1
    @RonInbar Note that those `Applicative` and `Monad` instances don't accumulate the output monoidally -- they amount to (strict) `StateT` rather than `AccumT`. – duplode Jul 01 '23 at 23:13
  • I updated the answer with a workaround. – K. A. Buhr Jul 02 '23 at 18:53
  • @duplode I'm trying to change it according to your comment but I keep getting the wrong results. I feel like such a novice trying to debug Haskell code... – Ron Inbar Jul 04 '23 at 13:52
  • @duplode How about [now](https://github.com/roninbar/nim/blob/accum/src/Main/Trans/Accum.hs)? – Ron Inbar Jul 04 '23 at 18:26
  • 1
    @RonInbar Those instances look okay now. By the way, though I might be missing something, I don't get why `Control.Monad.Accum` from *mtl* defines just the `Monoid w => MonadAccum w (AccumT w Identity)` instance, rather than `(Monad m, Monoid w) => MonadAccum w (AccumT w m)`. – duplode Jul 05 '23 at 01:37
  • 1
    @RonInbar P.S.: On the `MonadAccum` instance, it seems that will get sorted out in *mtl* eventually -- see [PR #141](https://github.com/haskell/mtl/pull/141). – duplode Jul 05 '23 at 01:52
  • @duplode Yes, I'm aware of that problem. – Ron Inbar Jul 05 '23 at 04:23
2

Only three base actions inspect the incoming accumulated value: accum, look, and looks. So if you want the argument to runAccumT, you must explicitly ask for it using one of those as your very first action.

> execAccumT (look >>= add) 7 :: IO (Sum Int)
Sum {getSum = 7}

It does seem a bit odd.

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380