Just retested; it looks like I forgot to set facStateS’ to use S.modify’ and was accidentally running it on S.modify.
From benchmarks, it should be comparable to accumulator fac, fold fac, and for fac (which incidentally pulls ahead at very large numbers, despite being relatively unperformant at small numbers, and still works decently despite running on default State monad).
There was also an error in the benchmarking code regarding for, wherein the factorial wasn’t computing the correct result.
https://paste.tomsmeding.com/7wtcxovu
Source code above for benchmarks.
https://paste.tomsmeding.com/tOLf1KNH
Final set of benchmarks.
So it does look that Control.Monad.Trans.State.Strict is approximately zero-cost, and can be used recklessly as a replacement for accumulating parameter idiom in concert with modify’. The for_ idiom actually outperforms both folds and straight accumulating parameter for large n.
I read about Strict State Monad and modify’ on Kowainik a long time ago, iirc, but it looks like they moved to a new host and the old post is gone. But strangely enough, I didn’t get these results the last time I benchmarked naive factorial across idioms. Probably user error on my part (forgot to get Control.Monad.Trans.Strict)
Space-leaking monad transformers have been a huge gripe of mine traditionally; i.e, Haskell really emphasizes its monads and unperformant monads with huge performance penalties are somewhat embarrassing.
It’s nice to know that at least StateT.Strict is safe, but that calls to mind, why isn’t StateT.Strict the default on Control.Monad.Trans.State? You can’t really hide modify to get people to use the “correct” modify’ first, but at least pointing Control.Monad.Trans.State to StateT.Strict would make life simpler and Haskell less footgunny; same applies to the mtl library version.