17 Comprehensions¶
{% include toc.html %}
In Elixir, it is common to loop over an Enumerable, often filtering out
some results and mapping values into another list. Comprehensions are
syntactic sugar for such constructs: they group those common tasks into
the for
special form.
For example, we can map a list of integers into their squared values:
iex> for n <- [1, 2, 3, 4], do: n * n
[1, 4, 9, 16]
A comprehension is made of three parts: generators, filters and collectables.
17.1 Generators and filters¶
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 in the right-hand side of the generator
expression:
iex> for n <- 1..4, do: n * n
[1, 4, 9, 16]
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:
iex> values = [good: 1, good: 2, bad: 3, good: 4]
iex> for {:good, n} <- values, do: n * n
[1, 4, 16]
Alternatively to pattern matching, filters can be used to filter some particular elements out. For example, we can get filter out all the multiples of 3 and get the square of the remaining values only:
iex> multiple_of_3? = fn(n) -> rem(n, 3) == 0 end
iex> for n <- 0..5, multiple_of_3?.(n), do: n * n
[0, 9]
Comprehensions filter out all elements for which the filter expression
returns false
or nil
; all other values are kept.
Comprehensions generally provide a much more concise representation than
using the equivalent functions from the Enum
and Stream
modules.
Furthermore, comprehensions also allow multiple generators and filters
to be given. Here is an example that receives a list of directories and
deletes all files in those directories:
for dir <- dirs,
file <- File.ls!(dir),
path = Path.join(dir, file),
File.regular?(path) do
File.rm!(path)
end
Keep in mind that variable assignments inside the comprehension, be it in generators, filters or inside the block, are not reflected outside of the comprehension.
17.2 Bitstring generators¶
Bitstring generators are also supported and are very useful when you need to comprehend over bitstring streams. The example below receives a list of pixels from a binary with their respective red, green and blue values and converts them into tuples of three elements each:
iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]
A bitstring generator can be mixed with the “regular” enumerable generators and provides filters as well.
17.3 Results other than lists¶
In the examples above, all the comprehensions returned lists as their
result. However, the result of a comprehension can be inserted into
different data structures by passing the :into
option to the
comprehension.
For example, a bitstring generator can be used with the :into
option
in order to easily remove all spaces in a string:
iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"
Sets, maps and other dictionaries can also be given to the :into
option. In general, :into
accepts any structure that implements the
Collectable
protocol.
A common use case of :into
can be transforming values in a map,
without touching the keys:
iex> for {key, val} <- %{"a" => 1, "b" => 2}, into: %{}, do: {key, val * val}
%{"a" => 1, "b" => 4}
Let’s make another example using streams. Since the IO
module
provides streams (that are both Enumerable
s and
Collectable
s), an echo terminal that echoes back the upcased
version of whatever is typed can be implemented using comprehensions:
iex> stream = IO.stream(:stdio, :line)
iex> 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. Unfortunately, this example also
got your IEx shell stuck in the comprehension, so you will need to hit
Ctrl+C
twice to get out of it. :)