Understanding Comprehensions in Elixir

The first time I saw a comprehension in Elixir I was a bit confused as to what the point of it was. Initially I didn’t understand why you would write a comprehension when you could achieve the same result using the existing tools that Elixir provides.

It was only after digging into some Elixir code that I realised that comprehensions just make a common action a bit nicer to read and write.

In today’s tutorial we will be looking at using comprehensions in Elixir.

What is a Comprehension?

A couple of weeks ago we looked at Enumerables in Elixir. The Enum module provides a number of very useful functions for working with enumerable data structures.

For example, imagine if you wanted to take each number in a range and multiple it by 2. You could do that using the Enum.map/2 function:

Enum.map(1..3, &(&1 * 2))

In this example I’m passing a function as the second argument. Each item in the range will be passed into the function, and then a new list will be returned containing the new values. We looked at this in functions as first class citizens in Elixir.

Mapping, filtering, and transforming are very common actions in Elixir and so there is a slightly different way of achieving the same result as the previous example:

for n <- 1..3, do: n * 2

If you run both examples in iex you will see that they produce the same result.

The second example is a comprehension, and as you can probably see, it’s simply syntactic sugar for what you could also achieve if you were to use the Enum.map/2 function.

When we looked at the choice between using the Enum module or the Stream module, we saw that both modules have the same functions with the same signatures. The difference is the Enum module will act on the data eagerly, whereas the Stream module will act on the data lazily. This will make a big difference if you are working with a large dataset.

However, there are no real benefits to using a comprehension over a function from the Enum module in terms of performance.

So whilst there’s no benefit other than the syntactic sugar, comprehensions are still very important to learn about because you will see them in other people’s Elixir code.

Using Generators

In the previous example I passed a range of 1..3 into the comprehension:

n <- 1..3

This chunk of the comprehension is known as the generator because it is generating values to be passed into the comprehension. In this example I’m passing a range into the right side of the generator.

In just the same in which you would use the functions of the Enum module, you can pass any enumerable data structure into the right side of the generator. For example, here is an example of getting the message from a keyword list of responses:

Inserting into something other than a list

In each of the examples we’ve looked at so far the comprehension has always returned a new list. If you want to return a different type of data structure you can do that using the :into option. The only requirement is that the data structure must implement the Collectable protocol (What are Elixir Protocols?).

For example, here I’ve got a map where I want to convert each value to begin with an uppercase character:

me = %{first_name: "philip", last_name: "brown"}

I could achieve this using a comprehension:

for {k, v} <- me, into: %{}, do: {k, String.capitalize(v)}

This will produce the following map:

%{first_name: "Philip", last_name: "Brown"}

Conclusion

Comprehensions are another way of working enumerables. They provide some nice syntactic sugar, but there is no real difference in terms of performance.

Comprehensions can use multiple generators for nested loops, pattern matching, and filters. You can also produce anything that is “collectable”.

I think the main benefit of comprehensions is readability. Whilst we’ve only looked at simple examples in this tutorial, you will likely see better examples in the wild that benefit from being written as a comprehension, especially when using mutliple generators or filters.