September 23, 2020

Elixir Comprehensions - The "for" Macro

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:

  1. Generators
  2. Filters
  3. Collectables (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

image.png

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

tags: programming elixir code macro comprehensions