Language Foundations

Bindings, Scope & Closures

Choose let or let mut, follow names through blocks, and trace a counter shared across closure calls.

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.

TOPAZ
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.

BASH
topaz check bindings-scope-closures.tpz
topaz run bindings-scope-closures.tpz

The check succeeds with exit code 0. The program prints these two lines.

Output
inner: 11, 12
outer

Follow 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.

StepValue of currentReturned value
makeCounter(10)10The closure
First next()1111
Second next()1212

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

FormUse it forReassignment
constA value computed at compile time from an allowed constant expressionNot allowed
letA name that keeps its assigned valueNot allowed
let mutA counter or other state that receives new valuesAllowed

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, a const, 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.