haskellmonad-transformers

How to use `runAccumT`


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?


Solution

  • 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})