A binding gives a value a name. Its declaration also answers two questions that matter later: may the name be reassigned, and where is it visible? A closure can keep that binding alive after the surrounding function has returned.
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.tpz
The two calls share one counter, while the inner label disappears with its block:
inner: 11, 12
outerFollow the binding, not a copied value
makeCounter creates the mutable binding current and returns a lambda. The lambda refers to current, so it captures that binding. Because the original declaration used let mut, assignment inside the lambda is valid.
The closure does not receive a frozen copy of 10. It retains the same mutable cell. The first call changes the cell to 11; the second call reads that new value and changes it to 12. The cell remains alive because next still refers to it after makeCounter has returned.
label demonstrates lexical scope. The inner block may shadow the outer name with its own binding. Inside the block, label means "inner". Once the block ends, that binding is out of scope and the outer "outer" binding is 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 later 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; they do not receive 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 first immutable and mutable bindings. Continue with Functions & Generics to choose between named functions, lambdas, and generic functions.