In this post we will be going over comprehensions in Elixir. A “Comprehension” is another word for Elixir’s for macro. It can be used to iterate through an enumerable, like Enum or Stream:
for element <- Enumerable do
element
end
In Elixir, it is common to loop over an Enumerable, often times filtering out some results and mapping values into another list. Comprehensions are the "syntactic sugar" for such constructs.
For example, we can map a list of integers into their squared values:
for n <- [1, 2, 3, 4], do: n * n
[1, 4, 9, 16]
The for macro has three parts:
Generators
Filters
Collectibles (The :into Option)
Generators
Generators are written like this:
element <- Enumerable
In the expression above, n <- [1, 2, 3, 4] is the generator. It is literally generating values to be used in the comprehension. Any enumerable can be passed on the right-hand side of the generator expression:
for n <- 1..4, do: n * n
[1, 4, 9, 16]
You can have multiple generators in a single for comprehension:
suits = [:hearts, :diamonds, :clubs, :spades]
faces = [2, 3, 4, 5, 6, 7, 8, 9, 10,
:jack, :queen, :king, :ace]
for suit <- suits,
face <- faces,
do: {suit, face}
Generator expressions also support pattern matching on their left-hand side; all non-matching patterns are ignored. Imagine that, instead of a range, we have a keyword list where the key is the atom :good or :bad and we only want to compute the square of the :good values:
values = [good: 1, good: 2, bad: 3, good: 4]
for {:good, n} <- values, do: n * n
[1, 4, 16]
Filters
Alternatively to pattern matching, filters can be used to select some particular elements. Filter expressions are written after generators like this:
for element <- Enumerable, filter do
element
end
For example, we can select the multiples of 3 and discard all others:
multiple_of_3? = fn(n) -> rem(n, 3) == 0 end
for n <- 0..5, multiple_of_3?.(n), do: n * n
[0, 9]
Comprehensions discard all elements for which the filter expression returns false or nil; all other values are selected.
You can have multiple filters:
for {suit, face} <- deck,
suit == :spades,
is_number(face),
face > 5,
do: {suit, face}
Comprehensions generally provide us with a much more concise representation than using the equivalent functions from the Enum and Stream modules.
:into
In the examples above, all the comprehensions returned lists as their result. But, the result of a comprehension can be inserted into different data structures by passing the :into option to the comprehension.
Return something other than a list with the :into option:
for {key, val} <- %{name: "Daniel", dob: 1991, email: "..."},
key in [:name, :email],
into: %{},
do: {key, val}
The above use case of :into is transforming values in a map, without touching the keys.
Let’s make another example below using streams. Fire up your IEx shell and insert the code below into it.
Since the IO module provides streams (that are both Enumerables and Collectables), an echo terminal that echoes back the upcased version of whatever is typed can be implemented using comprehensions:
stream = IO.stream(:stdio, :line)
for line <- stream, into: stream do
String.upcase(line) <> "\n"
end
Now type any string into the terminal and you will see that the same value will be printed in upper-case.
uniq: true
can also be given to comprehensions to guarantee the results are only added to the collection if they were not returned before. For example:
for x <- [1, 1, 2, 3], uniq: true, do: x * 2
[2, 4, 6]
Note: The targets must support the Collectable protocol.
Variable Scoping
All variables used in for are local:
name = "Daniel"
for name <- names do
String.upcase(name)
end
name # => "Daniel"
Enum vs. Stream vs. for
Try it!
Create a function, using for, which will return all the even numbers up to a given number.
Write a function, using for, which joins a list of binaries together with a separator.
Documentations
Resources