Collections reference
The container types: List (built in), std::hash_map::HashMap,
std::hash_set::HashSet, std::range::Range, and the std::iterator protocol
underneath for.
List<T>
Built in, with literal syntax: [1, 2, 3]. An empty literal needs a type
annotation (let xs: List<str> = [];).
impl List<type T> {
fun new(): List<T>
fun push(&mut self, own item: T)
fun pop(&mut self): Option<T>
fun insert(&mut self, index: usize, value: T) // panics out of bounds
fun remove(&mut self, index: usize): T // panics out of bounds
fun len(self): usize
fun is_empty(self): bool
fun iter(self): ListIterator<T> // the lazy cursor; see Iterator
fun get(self, index: usize): Option<T>
fun first(self): Option<T>
fun last(self): Option<T>
fun map<U>(self, fn: |T| U): List<U>
fun filter(self, predicate: |T| bool): List<T>
fun filter_map<U>(self, fn: |T| Option<U>): List<U> // both, one pass
fun find(self, predicate: |T| bool): Option<T>
fun fold<B>(self, init: B, fn: |B, T| B): B
fun for_each(self, fn: |T| void)
fun reverse(self): List<T>
fun sort_by(own self, compare: |T, T| Ordering): List<T> // stable
fun to_array<const N>(self): Option<[T; N]> // None unless len() == N
}
impl List<type T: Add + Default> { fun sum(self): T }
impl List<type T: Mul + Default> { fun product(self): T }
impl List<type T: Ord> { fun sort(self): List<T> } // stable
impl List<type T: PartialEq> {
fun contains(self, value: T): bool
fun index_of(self, value: T): Option<usize>
}
impl List<type T: PartialEq> with PartialEq {
fun eq(self, b: List<T>): bool // element-wise, length first
}
impl List<type T: Display> { fun join(self, separator: str): str }
impl List<type T> with Default { fun default(): List<T> } // []
Indexing is list[i]; iterate with for item in list (copies) or
for e in &mut list (in-place views; see the
memory model). list.iter() hands back a
ListIterator<T> — a cursor over a snapshot of the list, and the entry to
the adapter chain. The snapshot is rule 1 at work (the cursor
stores the list in a slot that outlives the call, so it copies), which means a
push after iter() is not walked, and that iter() itself costs a copy:
fun main() {
mut live = [1, 2];
mut cursor = live.iter();
live.push(3);
mut total = 0;
for value in cursor {
total += value; // 1 + 2 — the snapshot predates the push
}
print(total);
}
The methods that take self by value are pure — they return a new list and
leave the receiver alone. The mutating ones take &mut self: push,
push_many, pop, insert, remove.
“Leave the receiver alone” reaches the ELEMENTS, not just the spine: the list a
pure method returns never shares element storage with the receiver, so writing
through xs.map(f)[0] cannot show up in xs[0]. That is rule 1 (a value is
copied when it is stored), and it is why push and sort_by are written own
— they keep what they are given. A fresh value costs nothing there: own copies
a place, and a value with no other owner moves in.
fun main() {
let words = ["alpha", "beta", "gamma"];
let lengths = words.map(|word| word.len());
print(lengths.sum());
print(words.fold("", |joined, word| joined + word));
}
Searching & equality
find takes a predicate and short-circuits; contains and index_of take a
value and need T: PartialEq. Missing answers are None, never a panic.
A List<T: PartialEq> is itself PartialEq: == compares element-wise,
length first. Because the impl is conditional, lists nest
(List<List<i32>> compares through its elements) and a struct holding a
List field can [derive(PartialEq)].
fun main() {
let scores = [40, 91, 65];
print(scores.find(|n| n > 50).unwrap_or(0)); // 91
print(scores.contains(65)); // true
print(scores.index_of(65).unwrap_or(0)); // 2
print(scores.index_of(7).is_none()); // true
print(scores == [40, 91, 65]); // true
print(scores == [40, 91]); // false — length first
}
Ordering
sort orders by Ord; sort_by takes a comparator returning
Ordering. Both are stable — elements the comparator calls
Equal keep their input order — and both return a new list, as does reverse.
import std::{ io::print, compare::Ordering };
fun main() {
let ns = [10, 2, 1];
print(ns.sort()[0]); // 1
print(ns.reverse()[0]); // 1
print(ns.sort_by(|a, b| b.compare(a))[0]); // 10 — descending
}
Splicing
insert shifts the tail right (an index equal to len() appends); remove
shifts it left and returns the element. Both panic on an out-of-range
index, exactly as list[i] does — a bad index is a caller bug. Reach for
get when you don’t know whether an index is live.
fun main() {
mut xs = [1, 2, 4];
xs.insert(2, 3);
print(xs.len()); // 4
print(xs.remove(0)); // 1
print(xs[0]); // 2
}
Appending a run
push_many(items) appends every item of a run, in order: another list, a
fixed array [T; n], a HashSet (its members in insertion order) or any
iterator — whatever is
Items<T> (std::iterator). The run is taken whole first, so a list pushed
onto itself appends a copy of itself as it stood.
fun main() {
mut xs = [1, 2];
xs.push_many([3, 4]);
xs.push_many([5, 6].iter().map(|x| x * 10));
xs.push_many(xs);
print(xs.len()); // 12
}
A reactive list takes the same call as ONE write: ListCell::push_many is a
single Splice at the end, so its readers see one change for the batch
(std::reactive).
Joining
join renders each element through Display and puts
separator between them. It lives in std::display beside the bound it
needs, so it is the one List method that takes an import — and calling it
without one names the import in the error.
import std::{ io::print, display::Display };
fun main() {
print(["alpha", "beta", "gamma"].join(", ")); // alpha, beta, gamma
print([1, 2, 3].join("-")); // 1-2-3
}
Fixed arrays [T; n]
A fixed array (the tour introduces it) is a
value whose length is part of its type. std implements its traits once for
every length, so [u8; 4] and [str; 100] get the same surface:
==/!=when the element isPartialEq— equal when every position is. Two arrays of different lengths are different types, so==never meets them; a[derive(PartialEq)]struct with an array field derives. The array isEqwhen the element is, so aT: Eqtakes an[i32; 4].Hashablewhen the element is — an array keys aHashMapor joins aHashSet.Debugwhen the element is —T: Debugtakes an array, and it prints as aListdoes.Defaultwhen the element is — the element’s default at every position, so a[derive(Default)]struct with an array field derives. The array type heads the path:[i32; 4]::default()(default_of<[i32; 4]>()instd::defaultis the same call spelled generically).Json/FromJsonwhen the element is — a JSON array, decoded only at its own length (see encoding).Items<T>—xs.push_many(fixed)appends one.to_list(), aListcopy of the elements, anditer(), a cursor over a snapshot of them that opens the adapter chain. An array never coerces to aList; these are the conversions.- The way back is the list’s:
list.to_array()answersOption<[T; N]>, a copy when the list holds exactlyNelements andNoneotherwise.Ncomes from the type the result lands in or is written,xs.to_array<3>().
import std::{ hash_map::HashMap, iterator::Iterator };
fun main() {
let rgb: [u8; 3] = [255, 128, 0];
print(rgb == [255, 128, 0]); // true
mut names: HashMap<[u8; 3], str> = HashMap::new();
names.insert(rgb, "orange");
print(names.get([255, 128, 0]).unwrap_or("?")); // orange
mut xs = [1];
let more: [i32; 2] = [2, 3];
xs.push_many(more);
print(xs.len()); // 3
let doubled = rgb.iter().map(|c| c.as_i32() * 2).to_list();
print(doubled[0]); // 510
print(rgb.to_list().len()); // 3
let back: Option<[i32; 3]> = xs.to_array();
print(back.unwrap()[2]); // 3
print(xs.to_array<2>().is_none()); // true
}
The same door is open to your own traits: an impl head may bind the length
(impl [type T; _] with Count, or impl [type T; const N] to read it as N),
and a function may take one (fun total<const N>(values: [i32; N])). The
types chapter states the rules.
HashMap<K, V>
impl HashMap<type K: Hashable, type V> {
fun new(): HashMap<K, V>
fun insert(&mut self, key: K, value: V)
fun get(self, key: K): Option<V>
fun get_or_insert(&mut self, key: K, make: || V): V // the held value, or make() stored and answered
fun contains_key(self, key: K): bool
fun remove(&mut self, key: K)
fun len(self): usize
fun is_empty(self): bool
fun keys(self): List<K>
fun values(self): List<V>
fun entries(self): List<(K, V)>
}
impl HashMap<type K: Hashable, type V: PartialEq> {
fun contains_value(self, value: V): bool
}
impl HashMap<type K: Hashable, type V: PartialEq> with PartialEq // same keys, equal values; order ignored
impl List<(type K: Hashable, type V)> { fun to_map(self): HashMap<K, V> }
impl HashMap<type K: Hashable, type V> with Default {
fun default(): HashMap<K, V> // the empty map
}
A HashMap iterates in insertion order, on both backends: keys(),
values() and entries() walk the keys in the order they first arrived, an
overwrite keeps its key’s place, and a key removed and inserted again goes to
the end. The name says how it keys — by Hashable value — not that its order
is arbitrary. Equality follows the same line: two maps are == when they hold the same keys
with equal values, whatever order either was built in, so a struct holding a
map can [derive(PartialEq)]. Before v0.42 the type was std::map::Map; that name
had one release as a deprecated alias and is gone since v0.44, and a use of it is
refused with the new spelling.
Keys compare by value. Scalars work directly, and so does a backed enum — one with explicit backing values, which is that value at runtime, so the backing value is the key and no derive is needed:
import std::hash_map::HashMap;
enum Align { Start = "flex-start", End = "flex-end" }
fun main() {
mut widths: HashMap<Align, i32> = HashMap::new();
widths.insert(Align::Start, 1);
print(widths.get(Align::Start).unwrap_or(0)); // 1
}
An unbacked enum (enum Plain { A, B }) is not a key on its own: without a
backing value it lowers to an array, like a struct, so it needs the derive along
with every other aggregate. A tuple of Hashable elements is a key as it
stands (HashMap<(i32, str), V>). A struct, an unbacked or payload-carrying
enum, or a List key works as long as it is Hashable. Derive it:
import std::hash_map::HashMap;
import std::hash::Hashable;
import std::option::Option::{ self, Some, None };
[derive(Hashable)]
struct Point {
x: i32,
y: i32,
}
fun main() {
mut seen: HashMap<Point, str> = HashMap::new();
seen.insert(Point { x = 1, y = 2 }, "origin-ish");
// A fresh, distinct Point with equal fields hits.
match seen.get(Point { x = 1, y = 2 }) {
Some(let label) => print(label), // origin-ish
None => print("miss"),
}
}
A generic struct derives too: the derive binds each parameter a field
reaches under Hashable, so struct Tagged<T> { item: T, tags: List<T> }
is a key at Tagged<str>, and an argument that is not Hashable is
refused where it is used.
keys() returns the real Ks (in insertion order), and the key is snapshot
on insert, so mutating the original afterward can’t desync the map.
entries() pairs keys()/values() into one List<(K, V)> snapshot, so
walking both together needs no hand-zipping; contains_value (needing
V: PartialEq, unlike the rest of HashMap) is the value-side counterpart to
contains_key:
import std::hash_map::HashMap;
fun main() {
mut scores: HashMap<str, i32> = HashMap::new();
scores.insert("alice", 1);
scores.insert("bob", 2);
mut total = 0;
for entry in scores.entries() {
total = total + entry.1; // entry.0 is the key, entry.1 the value
}
print(total); // 3
print(scores.contains_value(2)); // true
print(scores.contains_value(9)); // false
}
get_or_insert is the entry idiom: the value held for key, or — on a miss,
and only there — the one make builds, inserted and then answered. The maker
runs once per key, at the call site:
import std::hash_map::HashMap;
fun main() {
mut lengths: HashMap<str, usize> = HashMap::new();
print(lengths.get_or_insert("alpha", || "alpha".len())); // 5 -- made and stored
print(lengths.get_or_insert("alpha", || 0)); // 5 -- held; the maker did not run
print(lengths.len()); // 1
}
HashSet<T>
impl HashSet<type T: Hashable> {
fun new(): HashSet<T>
fun insert(&mut self, value: T)
fun contains(self, value: T): bool
fun remove(&mut self, value: T)
fun len(self): usize
fun is_empty(self): bool
fun values(self): List<T>
fun union(self, other: HashSet<T>): HashSet<T>
fun intersection(self, other: HashSet<T>): HashSet<T>
fun difference(self, other: HashSet<T>): HashSet<T>
}
impl List<type T: Hashable> { fun to_set(self): HashSet<T> }
impl HashSet<type T: Hashable> with Default { fun default(): HashSet<T> } // the empty set
impl HashSet<type T: Hashable> with PartialEq // same members; order ignored
impl HashSet<type T: Hashable + Wire> with Wire // the list of its members
Value-keyed like HashMap (element T must be Hashable); for x in set
iterates the elements in insertion order (a duplicate insert keeps its value’s
place; a value removed and inserted again goes to the end). std::set::Set was its
name before v0.42, removed in v0.44 as Map was.
A set of Wire members is Wire: it crosses as the list of its members, in
insertion order, so a struct with a set field derives Wire (and a mirrored
store carries one, each member a boundary of its own — contains(x)).
union/intersection/difference are the standard set operations, each
returning a new HashSet and leaving both receivers untouched:
import std::hash_set::HashSet;
fun main() {
mut a: HashSet<i32> = HashSet::new();
a.insert(1);
a.insert(2);
a.insert(3);
mut b: HashSet<i32> = HashSet::new();
b.insert(2);
b.insert(3);
b.insert(4);
print(a.union(b).len()); // 4 -- {1, 2, 3, 4}
print(a.intersection(b).len()); // 2 -- {2, 3}
print(a.difference(b).len()); // 1 -- {1}
}
Memo<K, V>
impl Memo<type K: Hashable, type V> {
fun new(): Memo<K, V>
fun get_or_insert(self, key: K, make: || V): V
fun get(self, key: K): Option<V>
fun forget(self, key: K)
fun clear(self)
fun len(self): usize
}
A per-key cache of values made on first ask. get_or_insert answers what
is held for key, or runs make, inserts the result and answers that. get
is the passive read — it makes nothing. (get_or is the old name, kept one
release as a deprecated alias.)
import std::memo::Memo;
fun main() {
let squares: Memo<i32, i32> = Memo::new();
print(squares.get_or_insert(7, || 7 * 7)); // 49 -- made
print(squares.get_or_insert(7, || 0)); // 49 -- held; the maker did not run
squares.forget(7);
print(squares.len()); // 0
}
make runs at the call site, so a maker that reads an ambient context —
the rpc client, a theme — reads the caller’s. Its lifetime is not the
caller’s, though: what the maker builds outlives the caller. The memo keeps
it for the life of the program, past every owner the first call ran under, so a
derivation built in a maker is .cell_global(), and a lease (.cell(),
effect) belongs at the call site, on what the memo answers:
// The memo holds the program-lifetime cell of the handle; the view that asks
// takes its own lease, released with the view.
let bodies: Memo<str, SignalCell<Option<RemoteSource<MessageBody>>>> = Memo::new();
fun body_of(id: str): MemoCell<Option<MessageBody>> {
bodies
.get_or_insert(id, || client_cell().derive(|client| client.map(|c| c.get_message(id))).cell_global())
.and_then(|mirror| mirror)
.memo()
}
A .cell() inside the maker would be tied to the FIRST caller’s owner: it dies
with that view, and every later ask is answered with the dead cell — stale, and
never updating again. The compiler warns at a .cell() or an effect written
directly in a maker.
You do not need a memo to get one remote handle per id: a generated handle
stub already answers the same mirror for the same method and arguments (see the
services guide). Nothing is evicted by itself; forget
is yours, for a value that turned out wrong.
It holds a Shared table inside, so a Memo bound with let at module level
is written by every call site that reads it; no mut is needed.
The one-slot twin is Shared<Option<T>>::get_or_insert(make): the held value,
or make() stored and answered — the same rule about what make builds. The
unshared form is HashMap::get_or_insert(key, make) on a map you hold mut.
Hashable
A key’s value is turned into a Hash (a canonical key) by key.hash(). Three
routes reach it:
- Scalars (
str,bool, every sized numeric), a fixed array[T; n]andList/Optionof aHashable— implemented by std. - A backed enum — implemented by the compiler beside its
value()/parse()(see the types chapter), off the same opt-in: writing= "flex-start"is what makes the enum that value, and the value is the key. SoAlign::Start.hash()andAlign::Start.value().hash()are the same key, and a backed enum is accepted as a field of a derived type too. Aresourceenum is excluded — a resource cannot be hashed by value. [derive(Hashable)]— for a struct or an enum whose fields are allHashable(scalars,str,bool,List/OptionofHashable, a backed enum, or another derived type); a closure,HashSet,HashMap, orSharedfield is rejected. Writing it on a backed enum is harmless and does nothing.
You can also hand-write impl Hashable to key by a subset of fields — except on
a backed enum, whose impl the compiler already provides — and build your own
container by bounding on K: Hashable and keying a HashMap<Hash, …> yourself.
A Hash is opaque: you can hold it, compare two with ==, hash it again (it is
itself Hashable, being already a canonical key), and use it as a key. You
cannot read the value inside, which is what keeps the representation free to
change. Equal values hash equal, so == on two hashes answers “same key?”:
import std::hash::Hashable;
[derive(Hashable)]
struct Point {
x: i32,
y: i32,
}
fun main() {
let here = Point { x = 1, y = 2 };
print(here.hash() == Point { x = 1, y = 2 }.hash()); // true
print(here.hash() == Point { x = 9, y = 2 }.hash()); // false
}
Equality is the whole of a Hash’s operator surface — there is no order on a
canonical key, so < is a compile error rather than a lexicographic compare of
the underlying JSON.
One corner: a float inside an aggregate key canonicalizes through JSON, where
NaN becomes null and -0/+0 collapse to 0, so those collide. Bare
numeric keys don’t have this (they key by JS value directly).
Range
End-exclusive integer ranges, made for for:
Range::new(start: i32, end: i32): Range // start..end, end excluded
range.next(&mut self): Option<i32>
import std::range::Range;
fun main() {
mut total = 0;
for i in Range::new(1, 5) { // 1, 2, 3, 4
total += i;
}
print(total);
}
Iterator
The protocol for consumes, and the seam for custom sequences:
trait Iterator<T> { fun next(&mut self): Option<T>; }
Iterator::from_fn(fn: || Option<T>): IteratorFromFn<T> // an iterator from a closure
next takes &mut self because advancing is a mutation of the iterator’s own
state — a cursor, a counter, a running total. Implement it on a struct of yours
and the type works in a for loop, and satisfies an I: Iterator<T> bound:
import std::iterator::Iterator;
import std::option::Option::{ self, Some, None };
struct Countdown {
remaining: i32,
}
impl Countdown with Iterator<i32> {
fun next(&mut self): Option<i32> {
if self.remaining <= 0 {
None
} else {
self.remaining -= 1;
Some(self.remaining)
}
}
}
fun main() {
mut countdown = Countdown { remaining = 3 };
for n in countdown {
print(n); // 2, 1, 0
}
}
Range implements Iterator<i32>, and so does every adapter below.
One thing to know about the loop: for resolves the protocol on the method
name, so a type with a next(&mut self): Option<T> drives a loop whether or
not it declares the trait. Declaring it is what buys the adapters below — and
what lets a generic bound accept your type.
That resolution is ordinary method resolution, which means an inherited
default counts. If the trait supplies a next body and your impl block is
empty, your type still iterates — the same next you would reach by writing
value.next():
import std::option::Option::{ self, Some, None };
trait Countdown<T> {
fun tick(&mut self): Option<T>;
fun next(&mut self): Option<T> { self.tick() } // inherited by every impl
}
struct Down { at: i32 }
impl Down with Countdown<i32> {
fun tick(&mut self): Option<i32> {
if self.at <= 0 { None } else { self.at -= 1; Some(self.at) }
}
}
fun main() {
mut down = Down { at = 3 };
for n in down {
print(n); // 2, 1, 0
}
}
Precedence is the usual one: a next declared on the type itself beats an
inherited one, and two different traits offering same-named defaults is an
ambiguity the loop reports rather than resolving for you — a for has no way to
name a provider, so declare next on the type to settle it.
The name is what resolves, but the shape is still checked: the method has to
return an Option, because the loop stops at None. A next that returns
anything else is a compile error rather than a loop that quietly runs zero times
— or, worse, throws. The return annotation is optional and the rule does not
care: annotate it and the annotation is checked, leave it off and the body is,
so fun next(&mut self) { (self.fn)() } is fine (it yields an Option) and fun next(&mut self) { self.count += 1; } is not (it yields nothing).
What a for can iterate
Exactly two things:
- A type with
next(ornext_mut, forfor e in &mut it) — the protocol above, declared on the type or inherited from a trait default. - A natively iterable value:
List<T>, a fixed array[T; n], a tuple, astr(yielding its characters),HashSet<T>(insertion order), and any host type anexternal structnames.
Anything else is a compile error. A struct or enum of your own that provides no
next is not iterable — it has no meaning to fall back on, since a struct is
its fields and an enum is its variant tag at runtime:
struct Cursor { items: List<i32>, index: usize }
fun main() {
mut walked = Cursor { items = [1, 2], index = 0 };
for item in walked { // cannot iterate `Cursor`: it has no `next`
print(item);
}
}
HashMap is in that group: walk it through entries(), keys() or
values(), as the HashMap section above does.
Adapters
Every Iterator gets these, as trait defaults — implement next and you have
all of them:
fun map<U>(self, fn: |T| U): Mapped<Self, T, U>
fun filter(self, predicate: |T| bool): Filtered<Self, T>
fun filter_map<U>(self, fn: |T| Option<U>): FilterMapped<Self, T, U> // both, one pass
fun take(self, count: usize): Taken<Self, T>
fun skip(self, count: usize): Skipped<Self, T>
fun enumerate(self): Enumerated<Self, T> // (0, a), (1, b), …
fun zip<U, J: Iterator<U>>(self, other: J): Zipped<Self, J, T, U>
fun chain<J: Iterator<T>>(self, other: J): Chained<Self, J, T>
filter_map is map and filter at once: the projection answers
Some(value) to keep that value and None to drop the element, so a partial
or fallible projection needs neither a two-stage chain nor an Option to
unwrap afterwards. List carries the eager twin under the same name.
They are lazy: each returns a small struct holding its upstream, and nothing runs until something pulls. So a chain makes one pass over the source and builds no intermediate lists.
fun main() {
mut pipeline = [1, 2, 3, 4, 5, 6]
.iter()
.filter(|n| n % 2 == 0)
.map(|n| n * 10)
.take(2);
for value in pipeline {
print(value); // 20, 40
}
}
Laziness is what makes take more than a convenience: it never pulls past its
budget, so it bounds a source that has no end.
import std::iterator::Iterator;
import std::option::Option::{ self, Some, None };
struct Naturals {
at: i32,
}
impl Naturals with Iterator<i32> {
fun next(&mut self): Option<i32> {
self.at += 1;
Some(self.at)
}
}
fun main() {
mut squares = Naturals { at = 0 }.map(|n| n * n).take(3);
for value in squares {
print(value); // 1, 4, 9
}
}
zip stops with the shorter side, and enumerate numbers what reaches it —
put it after a filter and you get the positions in the output, not in the
source.
The adapter types are named in the past participle — Mapped, Taken,
Filtered — while the methods keep the plain names. That is deliberate: a verb’s
bare name can be a std type too (the hash map was Map until v0.42), and vilan’s
method resolution picks by registration order rather than reporting a collision,
so the type names stay out of each other’s way.
A for binding gets the element type the iterator was instantiated at, whatever
shape it is — a tuple, a struct, another container, a closure:
struct Point { x: i32, y: i32 }
fun main() {
for pair in [(1, "a"), (2, "b")].iter() {
print(pair.0); // 1, then 2
}
for point in [Point { x = 1, y = 2 }].iter() {
print(point.x); // 1
}
for inner in [[1, 2, 3], [4]].iter() {
print(inner.len()); // 3, then 1
}
}
Terminations
An adapter chain does nothing until it is terminated. These consume the iterator and hand back an ordinary value:
fun to_list(mut self): List<T>
fun fold<B>(mut self, init: B, fn: |B, T| B): B
fun for_each(mut self, fn: |T| void)
fun count(mut self): i32
fun any(mut self, predicate: |T| bool): bool // short-circuits on the first hit
fun all(mut self, predicate: |T| bool): bool // short-circuits on the first miss
fun rev(mut self): ListIterator<T> // a BARRIER — see below
to_list is the primary one, and it is deliberately explicit. A method that
names what it builds needs no type annotation, reads at the call site, and
works in the middle of an expression — xs.iter().filter(f).to_list().len() —
which is exactly where an inference-driven collect gives up. There is no
collect in vilan, by design; if one is ever added it will sit beside this
family, never replace it.
fun main() {
let evens = [1, 2, 3, 4, 5, 6].iter().filter(|n| n % 2 == 0).to_list();
print(evens.len()); // 3
print([1, 2, 3].iter().fold(0, |total, n| total + n)); // 6
print([1, 2, 3, 4].iter().filter(|n| n > 2).count()); // 2
print([1, 2, 3].iter().any(|n| n == 2)); // true
print([1, 2, 3].iter().all(|n| n > 0)); // true
}
any and all short-circuit, so they can answer over a source that has no end;
count, fold, for_each and to_list pull everything, so bound such a source
with take first.
rev is a barrier, not a lazy adapter: it drains its upstream into a List,
reverses that, and hands back a ListIterator. So the chain continues, but the
work up to that point has already happened — and rev never returns over an
unbounded source. (A lazy reverse needs a double-ended protocol, where every
adapter decides whether it can walk backwards. That is purely additive later:
rev’s signature would not change, only its body.)
For a HashSet or a HashMap, terminate with to_list() and convert:
impl List<type T: Hashable> { fun to_set(self): HashSet<T> }
impl List<(type K: Hashable, type V)> { fun to_map(self): HashMap<K, V> }
import std::hash_map::HashMap;
import std::hash_set::HashSet;
import std::option::Option::{ self, Some, None };
fun main() {
let unique = [1, 2, 2, 3].iter().filter(|n| n > 1).to_list().to_set();
print(unique.len()); // 2
let lengths = ["alpha", "hi"].iter().map(|word| (word, word.len())).to_list().to_map();
print(lengths.get("hi").unwrap_or(0)); // 2
}
These two live on List rather than on Iterator, and the reason is worth
knowing because it shapes what you can write yourself: to_set needs
T: Hashable, Iterator<T> does not bound T, and a trait default may not
require a bound its trait does not declare — nor can a method carry one of its
own. So a to_set written as a trait default is rejected at its own definition,
before any call. Putting it beside the bound it needs is the same choice join
makes with Display. A repeated key in to_map keeps the last pair, matching
insert.