Kernel.SpecialForms.for

You're seeing just the macro for, go back to Kernel.SpecialForms module for more information.

Comprehensions allow you to quickly build a data structure from an enumerable or a bitstring.

Let's start with an example:

iex> for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]

A comprehension accepts many generators and filters. Enumerable generators are defined using <-:

# A list generator:
iex> for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]

# A comprehension with two generators
iex> for x <- [1, 2], y <- [2, 3], do: x * y
[2, 3, 4, 6]

Filters can also be given:

# A comprehension with a generator and a filter
iex> for n <- [1, 2, 3, 4, 5, 6], rem(n, 2) == 0, do: n
[2, 4, 6]

Generators can also be used to filter as it removes any value that doesn't match the pattern on the left side of <-:

iex> users = [user: "john", admin: "meg", guest: "barbara"]
iex> for {type, name} when type != :guest <- users do
...>   String.upcase(name)
...> end
["JOHN", "MEG"]

Bitstring generators are also supported and are very useful when you need to organize bitstring streams:

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}]

Variable assignments inside the comprehension, be it in generators, filters or inside the block, are not reflected outside of the comprehension.

The :into and :uniq options

In the examples above, the result returned by the comprehension was always a list. The returned result can be configured by passing an :into option, that accepts any structure as long as it implements the Collectable protocol.

For example, we can use bitstring generators with the :into option to easily remove all spaces in a string:

iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"

The IO module provides streams, that are both Enumerable and Collectable, here is an upcase echo server using comprehensions:

for line <- IO.stream(), into: IO.stream() do
  String.upcase(line)
end

Similarly, 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:

iex> for x <- [1, 1, 2, 3], uniq: true, do: x * 2
[2, 4, 6]

iex> for <<x <- "abcabc">>, uniq: true, into: "", do: <<x - 32>>
"ABC"

The :reduce option

While the :into option allows us to customize the comprehension behaviour to a given data type, such as putting all of the values inside a map or inside a binary, it is not always enough.

For example, imagine that you have a binary with letters where you want to count how many times each lowercase letter happens, ignoring all uppercase ones. For instance, for the string "AbCabCABc", we want to return the map %{"a" => 1, "b" => 2, "c" => 1}.

If we were to use :into, we would need a data type that computes the frequency of each element it holds. While there is no such data type in Elixir, you could implement one yourself.

A simpler option would be to use comprehensions for the mapping and filtering of letters, and then we invoke Enum.reduce/3 to build a map, for example:

iex> letters = for <<x <- "AbCabCABc">>, x in ?a..?z, do: <<x>>
iex> Enum.reduce(letters, %{}, fn x, acc -> Map.update(acc, x, 1, & &1 + 1) end)
%{"a" => 1, "b" => 2, "c" => 1}

While the above is straight-forward, it has the downside of traversing the data at least twice. If you are expecting long strings as inputs, this can be quite expensive.

Luckily, comprehensions also support the :reduce option, which would allow us to fuse both steps above into a single step:

iex> for <<x <- "AbCabCABc">>, x in ?a..?z, reduce: %{} do
...>   acc -> Map.update(acc, <<x>>, 1, & &1 + 1)
...> end
%{"a" => 1, "b" => 2, "c" => 1}

When the :reduce key is given, its value is used as the initial accumulator and the do block must be changed to use -> clauses, where the left side of -> receives the accumulated value of the previous iteration and the expression on the right side must return the new accumulator value. Once there are no more elements, the final accumulated value is returned. If there are no elements at all, then the initial accumulator value is returned.