A binding associates a name with a value. Its declaration answers two essential questions: can the name be reassigned, and where is it visible? A closure can keep that binding alive even after the enclosing function returns.
Watch one captured binding change
Save this as bindings-scope-closures.tpz:
function makeCounter(start: int) -> () -> int {
let mut current = start
() => {
current = current + 1
current
}
}
let label = "outer"
let next = makeCounter(10)
{
let label = "inner"
print("{label}: {next()}, {next()}")
}
print(label)Check and run it:
topaz check bindings-scope-closures.tpz
topaz run bindings-scope-closures.tpzBoth calls share a single counter, while the inner label disappears when its block ends:
inner: 11, 12
outerFollow the binding, not a copied value
makeCounter creates the mutable binding current and returns a lambda. The lambda references current, capturing that binding directly. Because the original declaration used let mut, reassignment inside the lambda is valid.
The closure does not receive a frozen copy of 10. It retains the same mutable cell. The first call updates the cell to 11. The second call reads that new value and updates it to 12. The cell remains alive because next still refers to it after makeCounter returns.
label demonstrates lexical scope. The inner block may shadow the outer name with its own binding. Inside the block, label evaluates to "inner". Once the block ends, that binding goes out of scope and the outer "outer" binding becomes visible again. Redeclaring the same name twice in one scope is a static error.
Choose the binding deliberately
| Form | Use it for | Reassignment |
|---|---|---|
const | A compile-time value built from the restricted constant expression grammar | Not allowed |
let | An ordinary runtime value that keeps the same binding | Not allowed |
let mut | State whose reassignment is part of the model | Allowed |
Prefer let. Add mut only when a subsequent assignment is meaningful. This keeps changing state visible at the declaration instead of making every name potentially mutable.
Parameters and names introduced by patterns are immutable. If a function needs changing local state, copy the parameter into a new let mut binding with a name that describes that state.
Common mistake: treating capture as a snapshot
Capturing an immutable binding is read-only. Capturing a mutable binding preserves its live cell, so writes made through one closure are visible to later reads through the same binding. If two closures capture the same mutable binding, they share that cell rather than receiving independent counters.
Scope still applies when a closure is created. It may capture names that are lexically visible at that point, but it cannot reach a name declared later or a sibling block’s private binding.
Exact limits
- Inner scopes may shadow an outer name. One scope may not declare the same name twice.
- An immutable
let, aconst, a parameter, or a pattern binding cannot be assigned. - Mutable declarations accept a bare identifier rather than a destructuring pattern.
- Topaz closures have no explicit capture list, move-capture syntax, Rust closure traits, or borrow-receiver syntax.
- Resource lifetime is a separate decision. The File-only
usingform and deterministic cleanup belong in Defer & Resources.
Revisit Values and Functions for the initial immutable and mutable bindings. Continue with Functions & Generics to choose between named functions, lambdas, and generic functions.