When you’re writing functional code, you often map and filter collections of things. To make your life easier (and your code easier to read), Elixir provides a general-purpose shortcut for this: the comprehension.
The idea of a comprehension is fairly simple: given one or more collections, extract all combinations of values from each, optionally filter the values, and then generate a new collection using the values that remain.
The general syntax for comprehensions is deceptively simple:
result = for generator or filter… [, into: value ], do: expression
Let’s see a couple of basic examples before we get into the details.
| iex> for x <- [ 1, 2, 3, 4, 5 ], do: x * x |
| [1, 4, 9, 16, 25] |
| iex> for x <- [ 1, 2, 3, 4, 5 ], x < 4, do: x * x |
| [1, 4, 9] |
A generator specifies how you want to extract values from a collection.
| pattern <- enumerable_thing |
Any variables matched in the pattern are available in the rest of the comprehension (including the block). For example, x <- [1,2,3] says that we want to first run the rest of the comprehension with x set to 1. Then we run it with x set to 2, and so on. If we have two generators, their operations are nested, so
| x <- [1,2], y <- [5,6] |
will run the rest of the comprehension with x=1, y=5; x=1, y=6; x=2, y=5; and x=2, y=6. We can use those values of x and y in the do block:
| iex> for x <- [1,2], y <- [5,6], do: x * y |
| [5, 6, 10, 12] |
| iex> for x <- [1,2], y <- [5,6], do: {x, y} |
| [{1, 5}, {1, 6}, {2, 5}, {2, 6}] |
You can use variables from generators in later generators:
| iex> min_maxes = [{1,4}, {2,3}, {10, 15}] |
| [{1, 4}, {2, 3}, {10, 15}] |
| iex> for {min,max} <- min_maxes, n <- min..max, do: n |
| [1, 2, 3, 4, 2, 3, 10, 11, 12, 13, 14, 15] |
A filter is a predicate. It acts as a gatekeeper for the rest of the comprehension—if the condition is false, then the comprehension moves on to the next iteration without generating an output value.
For example, the code that follows uses a comprehension to list pairs of numbers from 1 to 8 whose product is a multiple of 10. It uses two generators (to cycle through the pairs of numbers) and two filters. The first filter allows only pairs in which the first number is at least the value of the second. The second filter checks to see if the product is a multiple of 10.
| iex> first8 = [ 1,2,3,4,5,6,7,8 ] |
| [1, 2, 3, 4, 5, 6, 7, 8] |
| iex> for x <- first8, y <- first8, x >= y, rem(x*y, 10)==0, do: { x, y } |
| [{5, 2}, {5, 4}, {6, 5}, {8, 5}] |
This comprehension iterates 64 times, with x=1, y=1; x=1, y=2; and so on. However, the first filter cuts the iteration short when x is less than y. This means the second filter runs only 36 times.
Because the first term in a generator is a pattern, we can use it to deconstruct structured data. Here’s a comprehension that swaps the keys and values in a keyword list.
| iex> reports = [ dallas: :hot, minneapolis: :cold, dc: :muggy, la: :smoggy ] |
| [dallas: :hot, minneapolis: :cold, dc: :muggy, la: :smoggy] |
| iex> for { city, weather } <- reports, do: { weather, city } |
| [hot: :dallas, cold: :minneapolis, muggy: :dc, smoggy: :la] |
A bitstring (and, by extension, a binary or a string) is simply a collection of ones and zeroes. So it’s probably no surprise that comprehensions work on bits, too. What might be surprising is the syntax:
| iex> for << ch <- "hello" >>, do: ch |
| 'hello' |
| iex> for << ch <- "hello" >>, do: <<ch>> |
| ["h", "e", "l", "l", "o"] |
Here the generator is enclosed in << and >>, indicating a binary. In the first case, the do block returns the integer code for each character, so the resulting list is [104, 101, 108, 108, 111], which IEx displays as ’hello’.
In the second case, we convert the code back into a string, and the result is a list of those one-character strings.
Again, the thing to the left of the <- is a pattern, and so we can use binary pattern matching. Let’s convert a string into the octal representation of its characters:
| iex> for << << b1::size(2), b2::size(3), b3::size(3) >> <- "hello" >>, |
| ...> do: "0#{b1}#{b2}#{b3}" |
| ["0150", "0145", "0154", "0154", "0157"] |
All variable assignments inside a comprehension are local to that comprehension—you will not affect the value of a variable in the outer scope.
| iex> name = "Dave" |
| Dave |
| iex> for name <- [ "cat", "dog" ], do: String.upcase(name) |
| ["CAT", "DOG"] |
| iex> name |
| Dave |
| iex> |
In our examples thus far, the comprehension has returned a list. The list contains the values returned by the do expression for each iteration of the comprehension.
This behavior can be changed with the into: parameter. This takes a collection that is to receive the results of the comprehension. For example, we can populate a map using
| iex> for x <- ~w{ cat dog }, into: %{}, do: { x, String.upcase(x) } |
| %{"cat" => "CAT", "dog" => "DOG"} |
It might be more clear to use Map.new in this case:
| iex> for x <- ~w{ cat dog }, into: Map.new, do: { x, String.upcase(x) } |
| %{"cat" => "CAT", "dog" => "DOG"} |
The collection doesn’t have to be empty:
| iex> for x <- ~w{ cat dog }, into: %{"ant" => "ANT"}, do: { x, String.upcase(x) } |
| %{"ant" => "ANT", "cat" => "CAT", "dog" => "DOG"} |
In Chapter 24, Protocols—Polymorphic Functions, we’ll look at protocols, which let us specify common behaviors across different types. The into: option takes values that implement the Collectable protocol. These include lists, binaries, functions, maps, files, hash dicts, hash sets, and IO streams, so we can write things such as
| iex> for x <- ~w{ cat dog }, into: IO.stream(:stdio,:line), do: "<<#{x}>>\n" |
| <<cat>> |
| <<dog>> |
| %IO.Stream{device: :standard_io, line_or_bytes: :line, raw: false} |