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.
| Form | Syntax | Result type |
|---|---|---|
| List comprehension | [expr for p in iter …] | List<T> |
| Stream comprehension | stream[expr for p in iter …] | Stream<T> |
| Map comprehension | {key: val for p in iter …} | Map<K, V> |
| Set comprehension | set{expr for p in iter …} | Set<T> |
| Generator expression | gen{expr for p in iter …} | existential Iterator<T> (some I: Iterator<Item = 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.
// A BOUNDED source, and `collect()` — those are what work today.
let primes = stream[n for n in 2..100 if is_prime(n)];
let found: List<Int> = primes.collect(); // -> List<Int>
stream[...]No combinators. primes.take(10) is refused with no method named 'take' found for type 'Stream<Int>', so an UNBOUNDED source —
stream[n for n in 2..] — cannot be cut down to a finite prefix and has
no consumer at all. Bound the range instead, as above.
The guard must be INLINE. stream[n for n in 2..100 if even(n)],
calling a free function, dies at run time with Type mismatch in call_closure: expected closure, got non-pointer. Written inline —
if n % 2 == 0 — it answers correctly, and the list comprehension
[n for n in 2..100 if even(n)] takes the function fine. It is the
stream lowering specifically.
collect() needs its binding annotated. let got = s.collect(); is
refused as not fully determined. Write let got: List<Int> =.
Stream comprehensions compose with stream-producing methods:
// `|> .method(args)` is real syntax (grammar: `pipe_method_call`), but
// `byte_stream` and `utf8_chunks` are not — a reader gets lines either
// from the text or from a buffered reader, whose `lines()` comes from
// the `BufRead` protocol.
let text = read_to_string(&path)?;
let errors = stream[l for l in text.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>.
// `split_once` answers `Maybe<(Text, Text)>` — the halves either side of
// the separator. There is no `Text.after_at`.
//
// A clause is `if <expression>`, `let <pattern> = <expression>`, or another
// `for` (grammar: `comprehension_clause`). There is no `if let`: the guard
// and the binding are two clauses, in that order.
let unique_domains = set{
pair.1
for email in addresses
if email.split_once(&"@") is Maybe.Some(_)
let pair = email.split_once(&"@").unwrap()
};
let clause does not filterlet Maybe.Some((_, domain)) = email.split_once(&"@") parses — a let
clause takes a pattern — but a row whose pattern does NOT match is still
produced rather than dropped. Filter with an if clause; bind with a
let one.
fn main() {
let addresses = ["a@x.test", "b@y.test", "no-at-sign"];
// `let` only — the row with no `@` is kept.
let unfiltered = set{
domain
for email in addresses
let Maybe.Some((_, domain)) = email.split_once(&"@")
};
// `if` first, then bind — the row with no `@` is dropped.
let filtered = set{
pair.1
for email in addresses
if email.split_once(&"@") is Maybe.Some(_)
let pair = email.split_once(&"@").unwrap()
};
print(unfiltered.len()); // 3
print(filtered.len()); // 2
}
Checked with verum run: 3 then 2.
Generators — gen{expr for ... }
The laziest of the container forms — no materialisation, no stream
plumbing, just the iterator protocol. A gen{…} expression evaluates
to a Generator<T>; a function can return it under that concrete
name or hide it behind an existential some I: Iterator<Item = T> —
Verum has no impl Trait-in-return-position sugar, some is the
real keyword (see
Generics → Existential types).
fn window_pairs<T: Clone>(xs: &List<T>) -> some I: Iterator<Item = (T, T)> {
gen{(xs[i].clone(), xs[i + 1].clone())
for i in 0..xs.len() - 1}
}
Consume a generator like any iterator — drive it with for, or
materialise it with .collect():
let g = gen{ i * i for i in 0..5 };
let squares: List<Int> = g.collect(); // [0, 1, 4, 9, 16]
One parser note: directly in an iterable position (for v in gen{…})
the { reads as the loop body — bind the generator to a let first,
or parenthesise (gen{…}). This is the same disambiguation rule as
record literals in if conditions.
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 the existential
some I: Iterator. There is no intermediate List allocated.
When to choose which
| If you need | Use |
|---|---|
| An in-memory collection now | list comprehension |
| Infinite or expensive inputs | stream comprehension |
| A lookup structure keyed by computed keys | map comprehension |
| Deduplication | set comprehension |
| An iterator to hand to another combinator | generator 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
- Patterns — destructuring in
for/letclauses. - Functions → generators — the
fn*form. stdlib/async—StreamAPI.stdlib/collections—List,Map,Set.