mf

How to Work With Elixir Comprehensions

Elixir is a very young programming language (emerged in 2011), but it is gaining popularity. I was initially interested in this language because when using it you can look at some common tasks programmers usually solve from a different angle. For instance, you can find out how to iterate over collections without the for cycle, or how to organize your code without classes.

Elixir has some very interesting and powerful features that may be hard to get your head around if you came from the OOP world. However, after some time it all starts to make sense, and you see how expressive the functional code can be. Comprehensions are one such feature, and this article I will explain how to work with them.

Comprehensions and Mapping

Generally speaking, a list comprehension is a special construct that allows you to create a new list based on existing ones. This concept is found in languages like Haskell and Clojure. Erlang also presents it and, therefore, Elixir has comprehensions as well.

You might ask how comprehensions are different from the map/2 function, which also takes a collection and produces a new one? That would be a fair question! Well, in the simplest case, comprehensions do pretty much the same thing. Take a look at this example:

Here I am simply taking a list with three numbers and producing a new list with all the numbers multiplied by 2. The map call can be further simplified as Enum.map( &(&1 * 2) ).

The do_something/1 function can now be rewritten using a comprehension:

This is what a basic comprehension looks like and, in my opinion, the code is a bit more elegant than in the first example. Here, once again, we take each element from the list and multiply it by 2. The el <- list part is called a generator, and it explains how exactly you wish to extract the values from your collection.

Note that we are not forced to pass a list to the do_something/1 function—the code will work with anything that is enumerable:

In this example, I am passing a range as an argument.

Comprehensions work with binstrings as well. The syntax is slightly different as you need to enclose your generator with << and >>. Let's demonstrate this by crafting a very simple function to "decipher" a string protected with a Caesar cipher. The idea is simple: we replace each letter in the word with a letter a fixed number of positions down the alphabet. I'll shift by 1 position for simplicity:

This is looking pretty much the same as the previous example except for the << and >> parts. We take a code of each character in a string, decrement it by one, and construct a string back. So the ciphered message was "elixir"!

But still, there is more than that. Another useful feature of comprehensions is the ability to filter out some elements.

Comprehensions and Filtering

Let's further extend our initial example. I am going to pass a range of integers from 1 to 20, take only the elements that are even, and multiply them by 2:

Here I had to require the Integer module to be able to use the is_even/1 macro. Also, I am using Stream to optimize the code a bit and prevent the iteration from being performed twice.

Now let's rewrite this example with a comprehension again:

So, as you see, for can accept an optional filter to skip some elements from the collection.

You are not limited to only one filter, so the following code is legit as well:

It will take all even numbers less than 10. Just don't forget to delimit filters with commas.

The filters will be evaluated for each element of the collection, and if evaluation returns true, the block is executed. Otherwise, a new element is taken. What's interesting is that generators can also be used to filter out elements by using when:

This is very similar to what we do when writing guard clauses:

Comprehensions With Multiple Collections

Now suppose we have not one but two collections at once, and we'd like to produce a new collection. For example, take all even numbers from the first collection and odd from the second one, and then multiply them:

This example illustrates that comprehensions may work with more than one collection at once. The first even number from collection1 will be taken and multiplied by each odd number from collection2. Next, the second even integer from collection1 will be taken and multiplied, and so on. The result will be: 

What's more, the resulting values are not required to be integers. For instance, you may return a tuple containing integers from the first and the second collections:

Comprehensions With the "Into" Option

Up to this point, the final result of our comprehension was always a list. This is, actually, not mandatory either. You can specify an into parameter that accepts a collection to contain the resulting value. 

This parameter accepts any structure that implements the Collectable protocol, so for example we may generate a map like this:

Here I simply said into: Map.new, which can be also replaced with into: %{}. By returning the {el1, el2} tuple, we basically set the first element as a key and the second as the value.

This example is not particularly useful, however, so let's generate a map with a number as a key and its square as a value:

In this example I am using Erlang's :math module directly, as, after all, all modules' names are atoms. Now you can easily find the square for any number from 1 to 20.

Comprehensions and Pattern Matching

The last thing to mention is that you can perform pattern matching in comprehensions as well. In some cases it may come in pretty handy.

Suppose we have a map containing employees' names and their raw salaries:

I want to generate a new map where the names are downcased and converted to atoms, and salaries are calculated using a tax rate:

In this example we define a module attribute @tax with an arbitrary number. Then I deconstruct the data in the comprehension using {name, salary} <- collection. Lastly, format the name and calculate the salary as needed, and store the result in the new map. Quite simple yet expressive.

Conclusion

In this article we have seen how to use Elixir comprehensions. You may need some time to get accustomed to them. This construct is really neat and in some situations can fit in much better than functions like map and filter. You can find some more examples in Elixir's official docs and the getting started guide.

Hopefully, you've found this tutorial useful and interesting! Thank you for staying with me, and see you soon.

Leave a Comment

Scroll to Top