Skip to content

Sequencing Actions for Their Effects

Here is something that mapM, zipWithM, foldM, and sequence have in common: They all perform a sequence of actions:

  • mapM f xs applies f to each element in xs. Each of these function applications is an action, and we perform these actions in the order in which the list elements occur in xs.

  • sequence fs executes each of the actions in fs, once again in the order in which these actions occur in fs.

  • zipWithM f xs ys is much like mapM f xs, only f takes two arguments, one from xs and one from ys. Once again, we apply f to the first element in xs and the first element in ys, then to the second element in xs and the second element in ys, and so on until one of the two lists runs out of elements.

  • foldM f init xs applies f once for each element in xs. In contrast to mapM, f actually takes two arguments, the current accumulator value and the current element in xs, and we update the accumulator value to whatever value f returns. But this does not change the fact that, once again, we run f once for each element in xs, in the order in which the elements occur in xs.

If we want to observe the results of pure functions such as map, foldl or zipWith, we need to inspect their return values. That's the only way pure functions can communicate their results to us—there are no side effects. That's no longer true for effectful functions such as mapM, foldM, zipWithM or sequence. In addition to producing return values, each of these functions has an effect, such as performing I/O or modifying the state of the State monad.

Sometimes, the effects of a sequence of actions are all we care about. We saw this in the previous subsection when using sequence to print a sequence of strings on screen:

GHCi
>>> sequence [putStrLn "Hello", putStrLn "world!"]
Hello
world!
[(),()]

We didn't really care about the list of return values [(),()] here. For such cases, we have

mapM_     :: Monad m => (a -> m b) -> [a] -> m ()
zipWithM_ :: Monad m => (a -> b -> m c) -> [a] -> [b] -> m ()
foldM_    :: Monad m => (b -> a -> m b) -> b -> [a] -> m ()
sequence_ :: Monad m => [m a] -> m ()

They all behave like their counterparts without underscores, but they return () (void). Their implementation is straightforward once you remember that we have the void function for functors:

void :: Functor f => f a -> f ()
void xs = () <$ xs

In the context of a monad, void f runs the action f and then throws away its return value.

With the help of this function, we obtain

mapM_ f xs        = void $ mapM f xs
zipWithM_ f xs ys = void $ zipWithM f xs ys
foldM_ f init xs  = void $ foldM f init xs
sequence_ fs      = void $ sequence fs

We've seen one example of using sequence_ already:

GHCi
>>> sequence_ [putStrLn "Hello", putStrLn "world!"]
Hello
world!

Similarly, we could print a list of strings by mapping putStrLn over the strings in a list:

GHCi
>>> mapM_ putStrLn ["Hello world!", "Hallo Welt!", "Hola mundo!"]
Hello world!
Hallo Welt!
Hola mundo!