Choose which names can receive new values and where those names are visible. Then follow a counter whose value changes each time you call the returned function.
Install Topaz using First Program. For the basics of let and functions, see Values and Functions.
Watch a counter change
Save this code as bindings-scope-closures.tpz in your working folder.
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)In that folder, check the file and run it.
topaz check bindings-scope-closures.tpz
topaz run bindings-scope-closures.tpzThe check succeeds with exit code 0. The program prints these two lines.
inner: 11, 12
outerFollow the two calls
A binding associates a name with a value. let mut current = start creates a binding that can receive a new value. makeCounter(10) starts it at 10 and returns the unnamed function written with =>, called a lambda.
The lambda keeps access to current from the surrounding function. This is capture, and a function that keeps this access is a closure. next stores that closure after makeCounter returns.
| Step | Value of current | Returned value |
|---|---|---|
makeCounter(10) | 10 | The closure |
First next() | 11 | 11 |
Second next() | 12 | 12 |
Both calls use the same binding. The second call adds one to the 11 left by the first. Captured bindings remain available while the closure needs them. Two closures that capture the same mutable binding also share its changes.
Find which name a block uses
Scope is the part of the code where a name is visible. Topaz uses lexical scope, so a name's declaration and the surrounding blocks determine which binding it refers to.
The inner let label hides the outer binding inside its block. This is shadowing. The first print reads "inner". Outside that block, print(label) reads "outer". Declaring the same name twice in one scope is a static error.
Choose whether a binding can change
| Form | Use it for | Reassignment |
|---|---|---|
const | A value computed at compile time from an allowed constant expression | Not allowed |
let | A name that keeps its assigned value | Not allowed |
let mut | A counter or other state that receives new values | Allowed |
Start with let and add mut when you need reassignment. Parameters and names introduced by patterns are immutable. To change a parameter's value locally, assign it to a new let mut binding, as makeCounter does with start.
Removing mut from current makes the assignment inside the lambda a static error. Restore let mut current in bindings-scope-closures.tpz to keep the counter working. Capturing a binding preserves its permission to change.
Capture and declaration rules
- A closure can capture names visible where it is created. Names declared later and names inside sibling blocks are unavailable.
- A mutable declaration uses a single identifier. Destructuring patterns cannot introduce mutable bindings.
- An immutable
let, aconst, a parameter, or a pattern binding cannot be reassigned inside a closure. - Explicit capture lists, move-capture syntax, Rust closure traits, and borrow-receiver syntax are not supported.
Keeping a captured binding alive does not decide when to close a file. See Defer & Resources for using with File and the rules for cleanup. Continue to Functions & Generics to choose named functions, lambdas, and generic functions.