Comprehensions in Elixir
A comprehension is Elixir's for special form, which combines one or more generators (x <- enumerable), optional filters, and a result expression into a single concise expression, e.g. for x <- [1, 2, 3], do: x * x produces [1, 4, 9]. Under the hood a comprehension is syntactic sugar over Enum/Stream operations, but it reads more like a mathematical set-builder notation, which makes nested loops, filtered transformations, and cross-products of multiple collections far more readable than the equivalent chain of Enum.map/Enum.filter/Enum.flat_map calls, especially once more than one generator is involved.
Cricket analogy: for x <- deliveries, x.runs >= 4, do: x mirrors a scorer filtering a full ball-by-ball log down to just the boundaries in one readable pass, instead of chaining multiple manual filters.
Generators, Filters, and Multiple Sources
A comprehension can have more than one generator, and every combination of their values is produced as a cross-product, so for x <- [1, 2], y <- [:a, :b], do: {x, y} yields [{1, :a}, {1, :b}, {2, :a}, {2, :b}] — invaluable for building a grid, a deck of cards, or every pairing between two lists. Filters are plain boolean expressions placed after the generators and separated by commas; every filter must evaluate truthy for a given combination to reach the do: block, and a generator's left-hand side can itself be a pattern that silently skips any element that fails to match, which combines filtering and destructuring in one step.
Cricket analogy: for team1 <- group_a, team2 <- group_b, do: {team1, team2} mirrors generating every possible group-stage fixture in a World Cup by pairing each Group A team against each Group B team.
The into: Option and Other Collectables
By default a comprehension collects its results into a list, but the into: option redirects the output into any structure that implements the Collectable protocol — into: %{} builds a map, into: "" concatenates strings (handy for building a string character by character), and into: MapSet.new() builds a set, all without an intermediate list allocation. Combined with the uniq: true option, which drops duplicate results, and the reduce: option for accumulating a single value across iterations instead of collecting a list, comprehensions cover a surprising range of use cases well beyond simple mapping.
Cricket analogy: for p <- players, into: %{}, do: {p.name, p.average} mirrors building a lookup map of batting averages keyed by player name directly from a squad list, instead of a list of pairs.
# Basic comprehension: list in, transformed list out
squares = for n <- 1..5, do: n * n
# [1, 4, 9, 16, 25]
# Filter: only even numbers, squared
for n <- 1..10, rem(n, 2) == 0, do: n * n
# [4, 16, 36, 64, 100]
# Multiple generators: cross-product
for suit <- [:hearts, :spades], rank <- [:ace, :king, :queen], do: {rank, suit}
# Pattern match in the generator filters out non-matching elements
results = [{:ok, 1}, {:error, :bad}, {:ok, 2}]
for {:ok, value} <- results, do: value
# [1, 2] - the {:error, :bad} tuple simply doesn't match, and is skipped
# into: builds a map instead of a list
for {k, v} <- [a: 1, b: 2], into: %{}, do: {k, v * 10}
# %{a: 10, b: 20}When a generator's pattern doesn't match an element — like {:ok, value} <- results skipping any {:error, _} tuples — Elixir does not raise a MatchError. Non-matching elements are silently excluded from the comprehension, which is a clean way to combine filtering and destructuring in a single generator clause.
Comprehensions with multiple generators produce a full cross-product, so nesting several large enumerables (for x <- big_list_1, y <- big_list_2, do: ...) can blow up combinatorially — a 10,000-element list crossed with another 10,000-element list produces 100 million results. Add filters early or restructure the logic before reaching for a comprehension at that scale.
- The for special form combines generators, filters, and a result expression into one readable construct.
- Multiple generators (x <- a, y <- b) produce every combination as a cross-product.
- Filters are boolean expressions after the generators; all must be truthy for a combination to be kept.
- A generator's pattern can itself filter: non-matching elements are silently skipped, no MatchError.
- into: redirects the result into any Collectable (map, string, MapSet) instead of a list.
- uniq: true removes duplicates; reduce: accumulates a single value instead of collecting results.
- Comprehensions with multiple large generators can produce a combinatorial explosion of results.
Practice what you learned
1. What does for x <- [1, 2], y <- [:a, :b], do: {x, y} produce?
2. What happens when a generator's pattern doesn't match an element, as in for {:ok, v} <- [{:ok, 1}, {:error, :x}], do: v?
3. What does the into: %{} option do in a comprehension?
4. What underlies Elixir's for comprehension under the hood?
5. Why should you be cautious with a comprehension using two very large generators?
Was this page helpful?
You May Also Like
Lists and Tuples in Elixir
Learn how Elixir's two core sequential data structures — singly linked lists and fixed-size tuples — are represented in memory and when to use each one.
Maps in Elixir
Understand Elixir's key-value data structure, how to build, update, and pattern-match against maps, and when to reach for structs instead.
Protocols in Elixir
Learn how Elixir protocols provide polymorphism across data types, letting a single function like Enum.map or to_string dispatch differently for lists, maps, and your own structs.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics