Kernel.SpecialForms.for
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.