06 — Mutation, regions, and escape
Previous: Types and strings
日本語版: 06_mutation.vibe.md
vibe is immutable by default, and mutation is deliberately small: it is local, and it never appears on a function's effect row. A function that uses a counter internally has the same signature as one that does not, because from the outside there is no difference.
A counter
let mut gives you a writable binding for the rest of its block:
fn main with Console {
let y = {
let mut v = 0
v += 1
v + 1
}
println("y = \{y}")
}
y = 2
y is an ordinary immutable Int. The mutable binding existed only
inside the braces, and the compiler keeps it in a plain wasm local —
no allocation happens for it.
Growing an array
Here the binding is immutable and the contents grow. Array::push
appends in place, so every name for that array sees the new element —
including a function you passed it to:
fn grow(xs: Array[Int]) -> Unit {
Array::push(xs, 9)
}
fn main with Console {
let xs = [
1
]
grow(xs)
println("length = \{Array::length(xs)}, last = \{Array::get(xs, 1)}")
}
length = 2, last = 9
This is worth being deliberate about: xs is shared, not copied. If you
want a private copy, make one.
A field you can write through
When the thing that changes is part of a value, declare the field mut.
Every alias observes the write, including one taken before it:
struct Counter {
mut n: Int
} derive (Show)
fn bump(c: Counter) -> Unit {
c.n = c.n + 1
}
fn main with Console {
let c = Counter::{
n: 10
}
let alias = c
bump(c)
bump(c)
println("c.n = \{c.n}, alias.n = \{alias.n}")
}
c.n = 12, alias.n = 12
Prefer a let mut local when a local is what you mean. A mut field is
a cell that anybody holding the value can write, which is a much larger
claim than a counter.
Escape is capture
There is one rule that decides whether a let mut is really local: if a
closure that can outlive the binding captures it, it is not. The
compiler then puts it on the heap so the closure can still reach it.
You do not have to guess which of your bindings that happened to:
vibe escapes file.vibe
It prints one line per escaping let mut, and empty output means every
let mut in the file is a plain local. There is a second form:
vibe escapes --strict file.vibe
The default answers "what does codegen do" — and when in doubt codegen
boxes, so the default over-reports. --strict answers "can that closure
actually reach this binding", subtracting cases where a name is merely
shadowed. Ask the default when you care about cost, --strict when you
care about who can write what.
Regions
Concurrency needs a stronger version of the same idea: a scratch value
belonging to a task group must not outlive the group. TaskGroup::run
mints a fresh region tag for that purpose and rejects a value escaping
through the body's return. Concurrency covers
it where the machinery makes sense.
Which one to reach for
Now that you have seen them:
| you want | use |
|---|---|
| a counter or accumulator in one function | let mut |
| to build an array, then read it | ArrayBuilder, then freeze it |
| to build text | StringBuilder |
| a value whose field changes over time | struct S { mut f: T } |
| state shared across calls, mediated | an effect and a handle |
The first row covers most code. Collections covers the builders.
Next: Structs, enums, and match.