Elixir Comprehensions - The "for" macro

image.png

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 we would want to filter out some results and map the values into another list.

Comprehensions are what allow us to achieve this goal.

Let's look at this example where we 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 consists of three parts:

  1. Generators
  2. Filters
  3. Collectables (The :into Option)

Enum vs. Stream vs. for

image.png

Generators

Generators are written like this:

element <- Enumerable

In the expression below, 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 use multiple generators in a single for comprehension. Here is an example of this:

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}

Generators also support pattern matching on their left-hand side. Non-matching patterns get 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 the rest:

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.

Like generators, you can also use 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, 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 locally scoped:

name = "Nolan"

for name <- names do
  String.upcase(name)
end

name # => "NOLAN"

Documentations

H2
H3
H4
3 columns
2 columns
1 column
11 Comments
Ecency