comprehensions

Learn how to use Elixir comprehensions to efficiently generate lists, maps, and strings from enumerables and bitstrings with generators and filters.

Elixir Comprehensions

Understanding Elixir Comprehensions

Elixir comprehensions provide a powerful and concise way to iterate over any enumerable or bitstring and construct new data structures. They are a fundamental tool for functional programming in Elixir, allowing for expressive data transformation.

Basic Comprehension Structure

The basic form of an Elixir comprehension involves a generator and a block of code to execute for each element. By default, comprehensions build lists. The syntax uses for, followed by generators, optional filters, and a do block.

# Basic list generation
> for num <- [1, 2, 3, 4], do: num * num
[1, 4, 9, 16]

Adding Filters to Comprehensions

Comprehensions can include filters to selectively process elements. Filters are boolean expressions that, if false, cause the current iteration to be skipped. This allows for conditional data inclusion.

# Comprehension with a filter for even numbers
> for num <- [1, 2, 3, 4], rem(num, 2) == 0, do: num * num
[4, 16]

Multiple Generators and Filters

Elixir comprehensions support multiple generators, enabling nested iteration. You can also combine multiple filters to create more complex selection criteria.

# Multiple generators for combining numbers and strings
> for num <- [1,2,3], str <- ["a", "b"], do: "#{num}#{str}"
["1a", "1b", "2a", "2b", "3a", "3b"]

# Multiple filters to refine results
> for num <- [1,2,3], str <- ["a", "b"], num !== 3, str !== "a", do: "#{num}#{str}"
["1b", "2b"]

Pattern Matching in Generators

Generators can utilize pattern matching, which is particularly useful when working with maps or tuples. This allows you to extract specific values directly.

# Pattern matching to extract ages from a map
> for {_, age} <- %{doug: 4, lucy: 6, ralf: 10}, do: age
[4, 6, 10]

Building Different Data Structures with into:

The into: option allows you to specify the target data structure, enabling the creation of maps, strings, or other Enumerable types directly from a comprehension.

# Building a map from a comprehension
> for num <- [1, 2, 3, 4], into: %{}, do: {num, num*num}
%{1 => 1, 2 => 4, 3 => 9, 4 => 16}

# Building a string from a comprehension
> for num <- [1, 2, 3, 4], into: "", do: "the square of #{num} is #{num * num}. "
"the square of 1 is 1. the square of 2 is 4. the square of 3 is 9. the square of 4 is 16. "

Working with Bitstrings

Comprehensions offer a streamlined syntax for iterating over bitstrings, allowing you to extract individual bytes or bits, and even perform pattern matching on bit sizes.

# Iterating over bytes in a bitstring
> for <<byte <- <<255, 12, 55, 89>> >>, do: byte
[255, 12, 55, 89]

# Pattern matching on bit size
> for <<bit::size(1) <- <<42, 12>> >>, do: bit
[0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0]

# Shorthand for extracting RGB components
> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
> for <<r::8, g::8, b::8 <- pixels >>, do: {r, g, b}
[{213,45,132},{64,76,32},{76,0,0},{234,32,15}]

Comprehensions with Streams

Comprehensions integrate seamlessly with streams, allowing for lazy processing of large datasets without loading everything into memory at once. This is crucial for efficient handling of I/O operations.

# Processing a stream of input lines
stream = IO.stream(:stdio, :line)
for line <- Enum.take(stream, 5), into: stream do
  String.upcase(line)
end

Further Reading