Skip to main content

Comprehensions

Verum has five forms of comprehension, all sharing the same clause grammar. The only thing that differs is the container — what the expression produces.

FormSyntaxResult type
List comprehension[expr for p in iter …]List<T>
Stream comprehensionstream[expr for p in iter …]Stream<T>
Map comprehension{key: val for p in iter …}Map<K, V>
Set comprehensionset{expr for p in iter …}Set<T>
Generator expressiongen{expr for p in iter …}impl Iterator<T>

The clause grammar is shared:

for pattern in iterable // draw elements
let pattern [: Type] = expr // bind an intermediate
if condition // filter

Any number of clauses can chain, in any order, after the first for.

Lists — [expr for ... ]

Eager. Produces a materialised List<T>.

let squares: List<Int> = [x * x for x in 0..10];

let evens = [n for n in 0..20 if n % 2 == 0];

let cartesian = [(a, b)
for a in 1..=3
for b in 1..=3
if a != b];

The compiler unifies the expression type to determine the element type; annotate the target binding to disambiguate if inference fails.

Streams — stream[expr for ...]

Lazy, pull-based. Returns a Stream<T> that produces elements on demand. Use when the input is infinite or large.

let primes = stream[n for n in 2.. if is_prime(n)];
let first_ten = primes.take(10).collect(); // -> List<Int>

Stream comprehensions compose with stream-producing methods:

let lines = file.byte_stream()
|> .utf8_chunks()
|> .lines();

let errors = stream[l for l in lines if l.starts_with("ERROR")];

See stdlib/async for Stream combinators (take, filter, map, chunk, throttle, …).

Maps — {key: val for ... }

Produces a Map<K, V>. Disambiguated from a map literal by the for keyword after the value expression.

let by_id = {user.id: user for user in users};

let word_lengths = {w: w.len() for w in words if !w.is_empty()};

// Swap keys/values:
let inverse = {v: k for (k, v) in original};

The key expression is evaluated first, then the value; duplicate keys follow the map's documented behaviour (see stdlib/collections).

Sets — set{expr for ... }

Prefix set disambiguates from a block and a map literal. Produces a Set<T>.

let unique_domains = set{
email.after_at()
for email in addresses
if email.is_valid()
};

Generators — gen{expr for ... }

Returns a generic impl Iterator<Item = T>. Laziest of the container forms — no materialisation, no stream plumbing, just an iterator protocol.

fn window_pairs<T: Clone>(xs: &List<T>) -> impl Iterator<(T, T)> {
gen{(xs[i].clone(), xs[i + 1].clone())
for i in 0..xs.len() - 1}
}

A generator expression is the simplest way to return an iterator without defining a named iterator type. For stateful iterators — those with yield points — define a generator function (fn*) instead. See Generators in functions.

Clauses in detail

for clause

Draws elements by pattern. The pattern follows the normal pattern grammar and can destructure tuples, records, and variants:

[user.name for User { name, active: true, .. } in users]

Multiple for clauses nest (leftmost is outermost):

[(a, b)
for a in xs
for b in ys] // Cartesian product of xs × ys

let clause

Binds an intermediate — avoids recomputing a value in the body and subsequent clauses:

[point
for raw in readings
let point: Point = parse_point(&raw)
if point.is_finite()]

if clause

Filters. Runs after all preceding for/let clauses are in scope:

[(x, y)
for x in 0..n
for y in 0..n
if x * x + y * y <= r * r] // disc of radius r

Stream literals

Beyond comprehensions, streams have literal syntax for common shapes:

let fives = stream[5, 5, 5, ...]; // infinite cycle of 5
let nats = stream[0, 1, 2, ...]; // pattern detected: 0, 1, 2, 3, ...
let counts = stream[0..]; // infinite upward range
let lazy_r = stream[0..100]; // lazy [0, 100)
let inc = stream[0..=100]; // lazy [0, 100]

Stream literals are desugared to constructors in stdlib/async; they are strictly convenience over Stream.from_iter, Stream.range, and friends.

Stream patterns

The companion to stream literals and comprehensions is pattern matching on stream prefixes:

match incoming_events {
stream[] => no_events(),
stream[ev] => single(ev),
stream[a, b, ...rest] => pair_plus(a, b, rest),
stream[...all] => consume_all(all),
}

...rest is an identifier that captures the remaining stream (still lazy). stream[...all] binds the entire stream without consuming.

Stream patterns consume elements lazily: stream[a, b, ...rest] pulls exactly two values and leaves rest available for further iteration.

Desugaring (for the curious)

A comprehension is equivalent to nested calls on the underlying iterator protocol. For example:

[f(x) for x in xs if p(x)]

desugars to:

xs.iter()
.filter(|&x| p(x))
.map(|x| f(x))
.collect::<List<_>>()

The compiler emits the nested form directly for the non-list containers: Stream, Map, Set, and impl Iterator. There is no intermediate List allocated.

When to choose which

If you needUse
An in-memory collection nowlist comprehension
Infinite or expensive inputsstream comprehension
A lookup structure keyed by computed keysmap comprehension
Deduplicationset comprehension
An iterator to hand to another combinatorgenerator expression

Grammar

From the grammar reference:

comprehension_expr = '[' , expression , 'for' , pattern , 'in' , expression
, { comprehension_clause } , ']' ;
map_comprehension = '{' , expression , ':' , expression
, 'for' , pattern , 'in' , expression
, { comprehension_clause } , '}' ;
set_comprehension = 'set' , '{' , expression , 'for' , pattern , 'in' , expression
, { comprehension_clause } , '}' ;
generator_expr = 'gen' , '{' , expression , 'for' , pattern , 'in' , expression
, { comprehension_clause } , '}' ;
stream_comprehension_expr = 'stream' , '[' , stream_body , ']' ;

comprehension_clause = 'for' , pattern , 'in' , expression
| 'let' , pattern , [ ':' , type_expr ] , '=' , expression
| 'if' , expression ;

See also