Advanced Exercism • julia

Comprehensions

Lesson Overview

# Introduction

About

Anyone who has used Python has almost certainly used list comprehensions, which have been central to Python syntax since early versions.

Something so convenient gradually finds its way into other languages, including Julia.

Comprehensions are an option rather than a necessity for Julia programmers, as there are usually alternatives (broadcasting, higher-order functions, etc).

However, a comprehension will often provide a simple, readable and performant way to construct an array. Use is ultimately a matter of personal taste, and how you feel about Python versus functional languages.

The syntax is mostly a direct copy of Python, but with extensions for higher-dimensional arrays.

Single variable

Very Pythonic, including the optional if clause.

julia> [x^2 for x in 1:3]
3-element Vector{Int64}:
 1
 4
 9

julia> [x^2 for x in 1:3 if isodd(x^2)]
2-element Vector{Int64}:
 1
 9

Output shape depends on the details of the comprehension: typically the same as the input collection in simple cases, but flattened to a Vector by an if clause.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> [x^2 for x in m]
2×3 Matrix{Int64}:
  1   4   9
 16  25  36

julia> [x^2 for x in m if isodd(x^2)] 
3-element Vector{Int64}:
  1
 25
  9

Multi-variable, Vector output

Like Python, we can have multiple for clauses with different variables, with the same or different input collections.

julia> [x * y for x in 1:3 for y in 4:6]
9-element Vector{Int64}:
  4
  5
  6
  8
 10
 12
 12
 15
 18

julia> [x * y for x in 1:3 for y in 4:6 if isodd(x * y)]
2-element Vector{Int64}:
  5
 15

This is equivalent to nested loops. The output is one-dimensional, even with matrix input.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> [x*y for x in m for y in m]
36-element Vector{Int64}:
  1
  4
  2
  5
(truncated output...)

Multi-variable, multi-dimensional output

The previous section described multiple for clauses separated only by spaces.

In this section, there is a single for and the variables are comma-separated.

julia> [(x, y) for x in 1:3, y in 4:6]
3×3 Matrix{Tuple{Int64, Int64}}:
 (1, 4)  (1, 5)  (1, 6)
 (2, 4)  (2, 5)  (2, 6)
 (3, 4)  (3, 5)  (3, 6)

Each variable in the comprehension creates a new dimension in the output, with an entry for each possible combination of the variables.

In the example above, x increases down the rows, y increases across the columns. Higher dimensions are possible, with the usual warnings about readability of the output.

Generator expressions

The previous sections have concentrated on array output, with the comprehension placed inside brackets [ ... ].

When the brackets are replaced by parentheses ( ... ) something different happens: we get a generator expression: a lazily-evaluated iterator which can yield the next value on demand.

julia> g = ((x, y) for x in 1:3, y in 4:6)
Base.Generator{Base.Iterators.ProductIterator{Tuple{UnitRange{Int64}, UnitRange{Int64}}}, var"#9#10"}(var"#9#10"(), Base.Iterators.ProductIterator{Tuple{UnitRange{Int64}, UnitRange{Int64}}}((1:3, 4:6)))

# Indexing fails with a generator, the entries don't exist yet
julia> g[1, 2]
ERROR: MethodError: no method matching getindex(::Base.Generator{Base.Iterators.ProductIterator{Tuple{UnitRange{…}, UnitRange{…}}}, var"#9#10"}, ::Int64, ::Int64)

# conversion to array
julia> collect(g)
3×3 Matrix{Tuple{Int64, Int64}}:
 (1, 4)  (1, 5)  (1, 6)
 (2, 4)  (2, 5)  (2, 6)
 (3, 4)  (3, 5)  (3, 6)

# generators are mainly designed for iteration
julia> [i * j for (i, j) in g]
3×3 Matrix{Int64}:
  4   5   6
  8  10  12
 12  15  18

When the result of a comprehension is immediately used for further processing, a generator can be memory-efficient, avoiding the need to store a large intermediate array.

A generator used as a function argument does not need additional parentheses.

# inefficient
julia> v = [x^2 for x in 1:1e6];

julia> sum(v)
3.333338333335e17

# better - use a generator
julia> sum(x^2 for x in 1:1e6)
3.3333383333312755e17

Generator syntax is identical to comprehensions, other than the surrounding brackets.

Generators can also be convenient in dictionary constructors.

julia> Dict(x => x^2 for x in 1:5)
Dict{Int64, Int64} with 5 entries:
  5 => 25
  4 => 16
  2 => 4
  3 => 9
  1 => 1

More advanced aspects of generators will be discussed in a future Interfaces concept (probably: planning is at an early stage!).


Originally from Exercism julia concepts