cult3

Understanding Comprehensions in Elixir

Jul 04, 2016

Table of contents:

  1. What is a Comprehension?
  2. Using Generators
  3. Using Filters
  4. Inserting into something other than a list
  5. Conclusion

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:

responses = [ok: "Hello World", error: "Server Error", ok: "What up"]

for {code, msg} <- responses, do: msg

As you can see from this example, we deconstruct each tuple of the keyword list into code and msg elements using pattern matching.

We can also use pattern matching to only choose certain elements of the data. For example, here I’m using pattern matching to only return the ok responses:

for {:ok, msg} <- responses, do: msg

You can also use multiple generators that act like nested loops. For example, if we have the following two lists:

one = [1, 2, 3]
two = [4, 5, 6]

We can combine them into a list of tuples like this:

for a <- one, b <- two, do: [a, b]

This will produce the following list:

[{1, 4}, {1, 5}, {1, 6}, {2, 4}, {2, 5}, {2, 6}, {3, 4}, {3, 5}, {3, 6}]

Here you can see we iterate through the one list and then iterate through the two list for each element.

Using Filters

A couple of weeks ago we looked at using pattern matching with functions. In that tutorial we saw how guards can be used to provide greater control.

If pattern matching doesn’t cut it when using a comprehension, you could also use a filter. A filter is basically the same as a guard.

For example, if you had the following list:

items = [:ok, 123, "hello world"]

We could create a new list by using the is_atom function:

for n <- items, is_atom(n), do: n

This will produce a new list containing only the :ok atom.

You can also pass in your own functions to be used as a filter. Here we have a function that checks to see if a number is divisible by 5:

divisible_by_5? = fn n -> rem(n, 5) == 0 end

We can pass this function as a filter just like we did in the previous example:

for n <- 1..100, divisible_by_5?.(n), do: n

This will produce the following list:

[5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]

You can also use multiple filters. For example, here I’ve added the is_even/1 function from the Integer module to also filter out any non-even numbers:

import Integer

for n <- 1..100, divisible_by_5?.(n), is_even(n), do: n

This will produce the following list:

[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

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.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.