13 — Collections
Previous: Writing tests
日本語版: 13_collections.vibe.md
Arrays, builders, and maps. vibe also has a naming rule that tells you which of them mutate, and you will be able to read it off the names by the end of this chapter.
Arrays
Array is the primitive sequence: index it, map over it, push to it.
import @vibe/builtin {
trait Iterator
}
fn main with Console {
let xs = [1, 2, 3]
println("xs[0] = \{xs[0]}")
println("length = \{Array::length(xs)}")
let doubled = Iterator::map(xs, _ * 2)
println("doubled[2] = \{Array::get(doubled, 2)}")
Array::push(xs, 4)
println("after push, length = \{Array::length(xs)}")
}
xs[0] = 1
length = 3
doubled[2] = 6
after push, length = 4
The one thing to remember: Array::push grows the receiver in place,
so every alias of that array sees the new length. An Array is a mutable
handle, not a value.
xs[i] and Array::get(xs, i) are the same read. Out of range, both
trap — the program stops rather than answering with a sentinel
(#2199 tracks making the trap say so). When "maybe absent" is the
normal case, that is what Option-returning lookups like MutMap::get
below are for.
Building one
When you fill a collection once and then only read it, build it and
freeze it. ArrayBuilder::freeze hands back an ordinary Array.
fn main with Console {
let b = ArrayBuilder::new()
ArrayBuilder::push(b, 10)
ArrayBuilder::push(b, 20)
let xs = ArrayBuilder::freeze(b)
println("built length = \{Array::length(xs)}, [0] = \{xs[0]}")
}
built length = 2, [0] = 10
Strings work the same way, and here it matters for a different reason: assembling a string with repeated concatenation is quadratic, while a builder is linear.
fn main with Console {
let b = StringBuilder::new()
StringBuilder::push(b, "hello ")
StringBuilder::push(b, "vibe")
println(StringBuilder::build(b))
}
hello vibe
Maps
MutMap is the general-purpose map, from @vibe/core. ::new_string
and ::new_int pick a key specialization, so the common cases need no
hash or equality closure from you.
import @vibe/core {
struct MutMap
}
fn unwrap_or(o: Option[Int], fallback: Int) -> Int {
match o {
Some(v) => v,
None => fallback
}
}
fn main with Console {
let m: MutMap[String, Int] = MutMap::new_string()
MutMap::set(m, "a", 1)
MutMap::set(m, "b", 2)
MutMap::set(m, "a", 7)
println("size = \{MutMap::size(m)}")
println("a = \{unwrap_or(MutMap::get(m, "a"), -1)}")
println("z = \{unwrap_or(MutMap::get(m, "z"), -1)}")
}
size = 2
a = 7
z = -1
get returns Option, so a missing key is a value you handle rather
than a crash or a zero. Setting a key that already exists replaces it —
"a" was set twice and the size stayed 2.
Reading mutability off the name
You have now used three of the four shapes. The rule behind them:
| spelling | meaning | examples |
|---|---|---|
| bare | persistent — an "update" returns a new value | Map |
| Mut- prefix | in-place handle | MutMap, MutSet |
| -Builder suffix | throwaway grower; finish it, then stop holding it | ArrayBuilder, StringBuilder |
| Frozen- prefix | immutable and allowed to cross a task boundary | FrozenArray[T] |
Persistent and Frozen are different questions. Persistent is about what
an update does; Frozen is about whether the value is Send — see
Concurrency.
Builders finish with ::build for StringBuilder, ::freeze for
ArrayBuilder and MapBuilder.
Array and Bytes are older than this rule and stay low-level mutable
primitives. Their operations mean the same thing on every backend.
Which map
Reach for MutMap. Map without a prefix is a small assoc list with
O(n) lookup — fine for a handful of keys, wrong for anything hot. For a
persistent map you intend to grow, use MapHamt from @vibex/immut.
If you meet HashMap in older code it is a transparent alias of
MutMap; vibe check warns on the old function names.
Next: Iteration.