There's nothing amazing about this code, it's pretty straight-forward
pattern matching Haskell. And at some point, many Haskellers end up
deciding that they don't like the explicit pattern matching, and
instead want to use a combinator. So the code above might get turned
into one of the following:

The theory is that all three approaches (maybe, mapM_, and
forM_) will end up being identical. We can fairly conclusively state
that forM_ will be the exact same thing as mapM_, since
it's just mapM_ flipped. So
the question is: will the maybe and mapM_ approaches do the same
thing? In this case, the answer is yes, but let's spice it up a bit
more. First, the maybe version:

You can compile and run this by saving to a Main.hs file and running
stack Main.hs && ./Main. On my system, it prints out the following
memory statistics, which from the maximum residency you can see runs
in constant space:

Notice how max residency has balooned up from 42kb to 132mb! And if
you increase the size of the generated list, that number grows. In
other words: we have linear memory usage instead of constant,
clearer something we want to avoid.

The issue is that the implementation of mapM_ in Data.Foldable is
not tail recursive, at least for the case of Maybe. As a result,
each recursive call ends up accumulating a bunch of "do nothing"
actions to perform after completing the recursive call, which all
remain resident in memory until the entire list is traversed.

Fortunately, solving this issue is pretty easy: write a tail-recursive
version of forM_ for Maybe:

There's one slight difference in the type of forM_Maybe and forM_
specialized to Maybe. The former takes a second argument of type a
-> m (), while the latter takes a second argument of type a -> m
b. This difference is unfortunately necessary; if we try to get back
the original type signature, we have to add an extra action to wipe
out the return value, which again reintroduces the memory leak:

Try swapping in this implementation into the above program, and once
again you'll get your memory leak.

mono-traversable

Back in 2014, I raised this same issue
about the mono-traversable library,
and ultimately decided to change the type signature of the omapM_
function to the non-overflowing demonstrated above. You can see that
this in fact works: