Two for One: Using concatMap

Today's for-loop replacement is a simpler one that combines two functions we should already know! We'll see how we can use concatMap to cover some of the basic loop cases we might encounter in other languages. This function covers the case of "every item in my list produces multiple results, and I want these results in a single new list." Let's write some C++ code that demonstrates this idea. We'll begin with a basic function that takes a single (unsigned) integer and produces a list of unsigned integers.

std::vector<uint64_t> makeBoundaries(uint_64t input) {
  if (input == 0) {
    return {0, 1, 2);
  } else if (input == 1) {
    return {0, 1, 2, 3};
  } else {
    return {input - 2, input - 1, input, input + 1, input + 2)
  }
}

This function gives the two numbers above and below our input, with a floor of 0 since the value is unsigned. Now let's suppose we want to take the boundaries of each integer in a vector of inputs, and place them all in a single list. We might end up with something like this:

std::vector<uint64_t> makeAllBoundaries(std::vector<uint64_t> inputs) {
  std::vector<uint64_t> results;
  for (uint64_t i : inputs) {
    std::vector<uint64_t> outputs = makeBoundaries(i);
    for (uint64_t o : outputs) {
      results.push_back(o);
    }
  }
  return results;
}

Here we end up with nested for loops in the same function! We can't avoid this behavior occurring. But we can avoid needing to write this into our source code in Haskell with the concatMap function:

concatMap :: (a -> [b]) -> [a] -> [b]

As the name implies, this is a combination of the map function we've already seen with an extra concatenation step. Instead of mapping a function that transforms a single item to a single item, the function now produces a list of items. But this function's "concat" step will append all the result lists for us. Here's how we could write this code in Haskell:

makeBoundaries :: Word -> [Word]
makeBoundaries 0 = [0, 1, 2]
makeBoundaries 1 = [0, 1, 2, 3]
makeBoundaries i = [i - 2, i - 1, i, i + 1, i + 2]

makeAllBoundaries :: [Word] -> [Word]
makeAllBoundaries inputs = concatMap makeBoundaries inputs

Nice and simple! Nothing in our code really looks "iterative". We're just mapping a single function over our input, with a little bit of extra magic to bring the results together. Under the hood, this function combines the existing "concat" and "map" functions, which use recursion. Ultimately, most Haskell for-loop replacements rely on recursion to create their "iterative" behavior. But it's nice that we don't always have to bring that pattern into our own code.

If you want to stay up to date with Haskell tips and tricks, make sure to subscribe to our monthly newsletter! We'll have some special offers coming up soon, so you won't want to miss them!

Previous
Previous

Using Scans to Accumulate

Next
Next

Try Fold First!