Collections
Verum's standard collections — List, Map, Set, Deque,
BinaryHeap, BTreeMap, BTreeSet — share a consistent
semantic-honest naming scheme. Every collection implements
Iterator, IntoIterator, Clone, Debug, Eq, and Default.
For the normative API surface of each type, see
stdlib/collections.
Constructing
// Empty
let xs: List<Int> = List.new();
let xs: List<Int> = [];
// Literal
let xs = [1, 2, 3, 4, 5]; // list literal
let ys = [0; 10]; // 10 copies of 0
let zs = [1, 2, 3]; // legacy list macro
// From iter
let fibs: List<Int> = (0..10).map(|n| fib(n)).collect();
// Constructor with capacity
let big: List<Int> = List.with_capacity(1024);
// From range
let r: List<Int> = (0..100).collect();
Similarly for Map, Set, etc.:
let m = Map.new();
let m = Map.from([("a", 1), ("b", 2)]);
let m = Map.from([("a", 1), ("b", 2), ("c", 3)]);
let s = Set.new();
let s = Set.from([1, 2, 3]);
let d = Deque.new();
let d = Deque.from_list(list);
Access
Call shapes below are API reference excerpts, not standalone programs.
xs[0] // panics on OOB
xs.get(i) // -> Maybe<&T>
xs.first() / xs.last() // -> Maybe<&T>
map[key] // panics on missing
map.get(key) // -> Maybe<V>
map.get_mut(&key) // -> Maybe<&mut V>, K: Clone
map.contains_key(key) -> Bool
set.contains(&value) -> Bool
Prefer .get over [] when the key might be absent — .get
returns Maybe and short-circuits elegantly with ?.
Map values and mutable access
get takes its key by value. Its Maybe<V> result is not a borrow of the
map's storage, although references inside V can still alias other
objects. get_mut takes a borrowed key and returns a mutable reference
to the existing value slot. Keep that borrow within the map's lifetime
and end it before insertions, removals, clearing, capacity changes or
other table rebuilding. Borrow syntax is not a guarantee that every
invalidated reference is detected.
The complete example below contrasts a scalar value returned by get
with a write through get_mut. It runs under the interpreter; native AOT
and other Map operations have separate
support limits.
mount core.collections.map.Map;
mount core.base.maybe.Maybe;
fn main() {
let mut counts: Map<Text, Int> = Map.new();
counts.insert("apple", 3);
let before = counts.get("apple").unwrap();
assert(counts.get("pear") is Maybe.None);
assert(counts.get_mut(&"pear") is Maybe.None);
match counts.get_mut(&"apple") {
Maybe.Some(value) => { *value = *value + 1; },
Maybe.None => panic("missing key"),
}
assert_eq(before, 3);
assert_eq(counts.get("apple").unwrap(), 4);
print("map-access: ok");
}
map-access: ok
Mutation
xs.push(value)
xs.pop() -> Maybe<T>
xs.insert(index, value)
xs.remove(index) -> T
xs.swap_remove(index) -> T // O(1); reorders
map.insert(key, value) -> Maybe<V> // returns the old value
map.remove(key) -> Maybe<V>
set.insert(value) -> Bool // true if newly inserted
set.remove(&value) -> Bool // true if was present
The entry API
The entry API is the shape the standard library declares for "insert or update":
let mut counts: Map<Text, Int> = Map.new();
for word in words.iter() {
*counts.entry(word.clone()).or_insert(0) += 1;
}
One lookup instead of the .contains_key + .get + .insert dance.
Known limitation: the interpreter's entry API is incomplete.
It can fail on populated maps as well as empty ones. The interpreter
carries a separate Map entry representation that source methods such
as entry, get_key_value and remove_entry cannot reliably read.
The entry snippets here describe the declared API; they are not supported
recipes. See Map support for the
current backend boundary.
Basic lookup and update operations are handled by the interpreter's runtime path; a successful lookup does not validate the entry API.
Use an explicit lookup followed by insertion:
mount core.collections.{Map, List};
fn main() {
let words: List<Text> = List.from([ "a", "b", "a" ]);
let mut counts: Map<Text, Int> = Map.new();
for word in words.iter() {
let n = counts.get_or(word.clone(), 0);
counts.insert(word.clone(), n + 1);
}
print(f"a={counts.get_or(\"a\", 0)} b={counts.get_or(\"b\", 0)}");
}
a=2 b=1
This costs a second lookup and avoids the incomplete entry API.
// Or-insert, or-insert-with, or-default:
map.entry(key).or_insert(default_value);
map.entry(key).or_insert_with(|| compute_default());
map.entry(key).or_default(); // T: Default
// And-modify:
map.entry(key).and_modify(|v| *v += 1).or_insert(1);
// Pattern match on the entry:
match map.entry(key) {
MapEntry.Occupied(mut entry) => {
*entry.get_mut() = new_value;
}
MapEntry.Vacant(entry) => {
entry.insert(default);
}
}
Iteration
Every collection iterates:
for x in &xs { ... } // iter() - &T
for x in &mut xs { ... } // iter_mut() - &mut T
for x in xs { ... } // into_iter() - owned T (consumes xs)
for (k, v) in &map { ... } // declared Map iteration yields (K, V)
for (k, v) in &mut map { ... } // &K, &mut V
for (k, v) in map { ... } // owned K, V
For Map, iter_mut and values_mut yield borrowed values, whereas
drain removes entries and yields owned (K, V) pairs. End iterators
and outstanding borrows before changing the table's structure. See
Map support for iterator limits.
Combinator stack
let primes: List<Int> = (2..1000)
.filter(|n| is_prime(*n))
.take(100)
.collect();
// Grouping is a method on the LIST, not an iterator adapter: there is
// no `into_group_map`. `group_by` answers a `Map<K, List<T>>` directly,
// so a count is `len()` over each group.
let groups: Map<Text, List<Email>> = emails.group_by(|e| e.domain());
let by_domain: Map<Text, Int> = groups
.into_iter()
.map(|(k, vs)| (k, vs.len()))
.collect();
Counting
let counts: Map<Text, Int> = words
.iter()
.fold(Map.new(), |mut acc, w| {
*acc.entry(w.clone()).or_insert(0) += 1;
acc
});
This uses the entry API, so it carries the same limitation as
The entry API above: it does not
run today. get_or + insert inside the fold works.
Or with a comprehension:
let counts = {w: words.iter().filter(|x| *x == w).count()
for w in set{x for x in &words}};
(See language/comprehensions.)
Grouping
// `List.group_by(&self, key_fn)` — on the list itself, and it already
// answers the map. `into_group_map_by` does not exist.
let grouped: Map<Category, List<Item>> =
items.group_by(|item| item.category.clone());
core.collections carries two free group_by functions besides the
method, and they differ in what they answer: the one in map.vr takes
an ITERATOR and answers Map<K, List<T>>, the one in list.vr takes a
&List and answers List<(K, List<T>)> — an association list, not a
map. Pick by the return type you want.
Or manually:
let mut groups: Map<Category, List<Item>> = Map.new();
for item in &items {
groups.entry(item.category.clone())
.or_insert_with(List.new)
.push(item.clone());
}
Same limitation as The entry API. Read the group with get_or, push, then insert it back.
Sorting
// List<T: Ord>
xs.sort(); // stable, in place
xs.sort_unstable(); // faster, not stable
xs.sort_by(|a, b| a.cmp(b)); // custom comparator
xs.sort_by_key(|x| key(x)); // T → K
// Produce a new sorted list:
let ys = xs.sorted(); // clone + sort
// BTreeMap/BTreeSet are already ordered — iterate in key order.
for (k, v) in &btree_map { ... }
Top-K
For the largest K items, BinaryHeap is O(n log k), not O(n log n):
// `BinaryHeap` already ships `top_k` and `bottom_k`; this is the shape
// they have, kept because writing it is how you learn the `Reverse`
// trick. Reach for `heap.top_k(k)` in real code.
fn top_k<T: Ord>(items: &List<T>, k: Int) -> List<T> {
let mut heap: BinaryHeap<Reverse<T>> = BinaryHeap.with_capacity(k);
for item in items.iter() {
heap.push(Reverse(item.clone()));
if heap.len() > k { heap.pop(); }
}
heap.into_sorted_list().into_iter().map(|Reverse(x)| x).collect()
}
Reverse<T> turns a max-heap into a min-heap. BinaryHeap (max by
default) + Reverse is the idiomatic top-K.
Deduplication
// Preserves order:
let deduped: List<_> = xs.iter().cloned().collect<Set<_>>().into_iter().collect();
// Sorted order:
xs.sort();
xs.dedup(); // remove consecutive duplicates
// By key:
xs.dedup_by_key(|x| x.id);
Sliding windows and chunks
xs.windows(3) // &[T; 3]... for each of size 3
.map(|w| w.iter().sum<Int>())
.collect<List<_>>()
xs.chunks(5) // &[T]... fixed chunks
.for_each(|chunk| process(chunk))
Set operations
let a: Set<Int> = Set.from([1, 2, 3]);
let b: Set<Int> = Set.of(2, 3, 4);
// All four return a `Set<T>` outright — built eagerly, not a lazy view.
// There is no iterator here to `.cloned().collect()`.
let union: Set<Int> = a.union(&b);
let intersection: Set<Int> = a.intersection(&b);
let difference: Set<Int> = a.difference(&b);
let symmetric: Set<Int> = a.symmetric_difference(&b);
let is_subset = a.is_subset(&b);
let is_superset = a.is_superset(&b);
let is_disjoint = a.is_disjoint(&b);
Deque — front and back
let mut d: Deque<Int> = Deque.new();
d.push_back(1);
d.push_back(2);
d.push_front(0); // [0, 1, 2]
d.pop_front(); // Maybe.Some(0)
d.pop_back(); // Maybe.Some(2)
d.rotate_left(3); // rotate
d.rotate_right(3);
BTreeMap — ordered map
let mut m: BTreeMap<Int, Text> = BTreeMap.new();
m.insert(3, "three");
m.insert(1, "one");
m.insert(2, "two");
for (k, v) in &m { ... } // 1 → one, 2 → two, 3 → three
// Range queries:
for (k, v) in m.range(1..3) { ... } // 1 and 2
for (k, v) in m.range(..=2) { ... }
for (k, v) in m.range(2..) { ... }
Use BTreeMap when you need ordered iteration or range
queries. Map (hashed) is faster for plain key lookup but
doesn't maintain order.
Capacity and reallocation
let mut xs: List<Int> = List.with_capacity(1000);
xs.reserve(500); // ensure 500 more fit
xs.shrink_to_fit(); // shrink unused
xs.shrink_to(100); // shrink to at least 100
let cap = xs.capacity();
let n = xs.len();
Pre-allocating avoids rehashing/regrowth overhead when you know the final size.
Memory layout notes
Per semantic honesty:
List<T>might be a contiguous buffer, a ring buffer, or a rope — chosen by profile-guided optimisation. Don't rely on specific layout.Map<K, V>might be Swiss-table, B-tree, or perfect hash.Set<T>isMap<T, ()>under the hood.Deque<T>andBinaryHeap<T>commit to their implementation by name.
The with [...] clause is capability attenuation — it narrows what
a value may be used for (Read, Write, Network, FileSystem, or a
capability you name yourself), not how it is laid out:
type ReadOnlyLog is List<Text> with [Read];
For a specific layout there is no knob on the semantic types. Use a
packed byte-buffer construction
for byte-oriented FFI: a type such as [Byte; 64] alone does not establish
the storage layout of an arbitrary returned value. @repr(C) pins a
record's field order for FFI. The supported
array-to-list return conversion
allocates list storage and copies elements; its result is not a promise of
contiguous raw bytes.
Pitfalls
xs.remove(i) is O(n)
Shifts every later element. For O(1) removal that doesn't preserve
order, use .swap_remove(i).
map[&k] panics on missing
Use .get(&k) and match/? instead of map[&k] in production
code.
Iterator invalidation
Mutating a List/Map invalidates borrows into it:
let first = xs.first(); // &T
xs.push(value); // COMPILE ERROR: &xs active
Move the borrow out of scope, or restructure.
Don't clone() a collection to read
// Wrong — clones the whole thing:
for x in xs.clone() { ... }
// Right:
for x in &xs { ... }
See also
stdlib/collections— normative API reference.stdlib/base—Iterator, adapters.- language/types — collection types in the type grammar.
- Comprehensions — list/map/set comprehensions.
- Generics — parameterising over collection element types.