#set document(title: "3.5 Closures", author: "Modular Inc. / XYZ Homework") #set page(width: 8.5in, height: auto, margin: 1in) #import "@preview/cetz:0.5.2" #set text(font: ("STIX Two Text", "Libertinus Serif", "New Computer Modern"), size: 10.5pt, lang: "en") #show math.equation: set text(font: ("STIX Two Math", "New Computer Modern Math")) #set par(justify: true, leading: 0.62em, spacing: 0.9em) #set enum(spacing: 1.1em) // room between list items so tall inline fractions don't collide #set list(spacing: 1.1em) #set table(stroke: 0.5pt + rgb("#c7ccd3")) #let BLUE = rgb("#183B6F") // brand navy — section bars + example/solution labels (white on navy 11.09:1) #let ORANGE = rgb("#A94509") // brand primary-700 — AA-safe deep orange for TEXT (5.93:1 on white; raw brand #F37021 is 2.94:1 and must never carry text) #let RED = rgb("#DC2626") // brand error-600 #let GREEN = rgb("#059669") // brand success-600 (decoration only; small green text uses green-text #007942) #show heading.where(level: 1): it => block(width: 100%, above: 0pt, below: 16pt, fill: gradient.linear(BLUE, rgb("#2C5AA0")), inset: (x: 14pt, y: 12pt), radius: 3pt, text(fill: white, weight: "bold", size: 19pt, it.body)) #show heading.where(level: 2): it => block(width: 100%, above: 18pt, below: 10pt, fill: BLUE, inset: (x: 10pt, y: 6pt), radius: 2pt, text(fill: white, weight: "bold", size: 12pt, it.body)) #show heading.where(level: 3): it => text(fill: ORANGE, weight: "bold", size: 12.5pt, it.body) #show heading.where(level: 4): it => text(fill: BLUE, weight: "bold", size: 10.5pt, it.body) #let examplebox(label, title, body) = block(width: 100%, breakable: true, fill: rgb("#EFF1F5"), stroke: 0.5pt + rgb("#CFDDF0"), radius: 4pt, inset: 10pt, above: 12pt, below: 12pt)[ #block(below: 6pt)[#box(fill: BLUE, inset: (x: 6pt, y: 2pt), radius: 2pt, text(fill: white, weight: "bold", size: 8.5pt, label)) #h(0.4em) #strong[#title]] #body] // rail = decorative left rule (raw brand token); labelcolor = AA-safe label text shade #let notebox(label, rail, labelcolor, tint, body) = block(width: 100%, breakable: true, fill: tint, stroke: (left: 3pt + rail), inset: (left: 10pt, rest: 8pt), radius: (right: 4pt), above: 11pt, below: 11pt)[ #text(fill: labelcolor, weight: "bold", size: 7.5pt, tracking: 0.5pt)[#upper(label)] #linebreak() #body] #let solutionbox(body) = block(above: 4pt, below: 8pt)[ #text(fill: BLUE, weight: "bold", size: 8.5pt)[Solution] #linebreak() #body] #let figph(msg) = block(width: 100%, height: 60pt, fill: rgb("#f6f7f9"), stroke: (paint: rgb("#c7ccd3"), dash: "dashed"), radius: 4pt, inset: 10pt)[ #align(center + horizon, text(fill: rgb("#889"), style: "italic", size: 9pt, msg))] // Standardize inlined figure sizes: measure the natural CeTZ canvas, then scale to a // consistent envelope (aspect-aware; see build_typst.py FIG_* constants). Unlike the // print preamble, dimensions are FLOORED: in an editor a user can trim a figure to a // degenerate 1-D shape (a bare line), and w/h or tw/w would then divide by zero. #let _STD_W = 3.5 #let _WIDE_W = 5.6 #let _MAX_H = 3.4 #let _ASPECT_WIDE = 2.2 #let _UPSCALE_MAX = 1.15 #let stdfig(body) = context { let m = measure(body) let w = calc.max(m.width / 1in, 0.01) let h = calc.max(m.height / 1in, 0.01) let tw = if w / h > _ASPECT_WIDE { _WIDE_W } else { _STD_W } let s = calc.min(tw / w, _MAX_H / h, _UPSCALE_MAX) align(center, box(scale(x: s * 100%, y: s * 100%, reflow: true, body))) } #show figure: set block(breakable: false) #set figure(gap: 8pt) #show figure.caption: set text(size: 8.5pt, fill: rgb("#555")) == 3.5#h(0.6em)Closures #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ #emph[Evolving feature] Closures are a longstanding part of Mojo, used throughout the standard library and kernel infrastructure. Mojo's updated capture-list syntax is now available. Code examples on this page reflect the redesigned compiler behavior. ] A #emph[closure] is a function bundled together with values from its surrounding scope. You define it in one place and pass it somewhere else to run. The closure carries captured data with it. The compiler transforms it to a type with both behavior and storage. The code executing the closure doesn't need to know where that data came from. This allows closures to carry state and configuration into code that executes later or elsewhere. Configuration uses capture conventions to specify how the closure interacts with captured values, choosing between read-only references, mutable references, copies, or moves. Mojo closures look like nested functions, but they use a special syntax. Curly braces after the argument list form a #emph[capture list] that declares which outer values the closure captures and how it interacts with them. The capture list is what distinguishes a closure from an ordinary nested function. It gives the compiler the information needed to manage captured values safely and eliminate ambiguity. When you specify how a value is captured, the compiler can enforce correct usage: def main(): var multiplier = 3 def scale(x: Int) {read multiplier} -\> Int: return x \* multiplier print(scale(5)) \# 15 15 scale is a closure. It captures multiplier from the enclosing scope using its capture list ({read multiplier}). When you call scale(5), the closure multiplies 5 by the captured value of multiplier and returns 15. Without a capture list, the inner function can't see anything outside its own arguments. === Why closures matter Closures package behavior together with the data that behavior needs. You define the work in one place, then pass that package for execution later, elsewhere, or on different hardware. This separation between defining work and executing work is central to how Mojo expresses computation. Closures already appear throughout the Mojo standard library. Functions such as parallelize, vectorize, and elementwise accept closures. You describe the work; the library decides how to distribute it across SIMD lanes, threads, or cores. GPU kernels extend the same idea. A closure captures the values a kernel needs, then executes across a hardware dispatch geometry. This pattern appears in reductions and accumulations, where a closure captures running state and updates it while processing elements. It also appears in custom iteration, where the iterator controls traversal and the closure controls behavior. In many languages, closures also power asynchronous programming. You pass a closure as a callback that runs after an operation completes, carrying the state and cleanup logic it needs. Mojo doesn't yet support async execution or escaping closures (closures that outlive their enclosing scope), but the underlying model is the same: closures define what should happen, while something else decides when and where it runs. === The capture list A capture list such as {read x, mut y} tells the compiler how the closure captures and uses values from the surrounding scope. For this list, x is captured as an immutable reference. The closure can read it but can't modify it. y is captured as a mutable reference, so the closure can modify it and those changes are visible in the outer scope. Mojo requires captures to be explicit. In a systems language, knowing exactly which values a closure holds, and whether it reads, copies, or takes ownership of them, matters for both performance and correctness. === Capture by immutable reference: read Use read when the closure needs to see a value but not change it: def main(): var threshold = 100 def is\_over(x: Int) {read threshold} -\> Bool: return x \> threshold print(is\_over(50)) \# False print(is\_over(200)) \# True False True The closure reads an immutable reference to threshold. It sees the current value each time it's called, including changes made after the closure was created: def main(): var limit = 10 def check(x: Int) {read limit} -\> Bool: return x \< limit print(check(5)) \# True limit = 3 print(check(5)) \# False (sees updated limit) True False Because read captures a reference, the closure reflects the live state of the original value. To capture every used outer value by immutable reference without naming each one, use \`\`: def main(): var a = 1 var b = 2 def sum\_ab() {read} -\> Int: return a + b print(sum\_ab()) \# 3 3 === Capture by mutable reference: mut Use mut when a closure needs to modify a captured value and make those changes visible in the enclosing scope: def main(): var total = 0 def accumulate(x: Int) {mut total}: total += x accumulate(10) accumulate(20) print(total) \# 30 30 Changes to total inside the closure modify the original variable directly. This is a mutable reference, not a copy. The implicit form \`\` captures every used outer value with a mutable reference: def main(): var count = 0 var items = List\[String\]() def record(name: String) {mut}: items.append(name) count += 1 record("alpha") record("beta") print(count) \# 2 print(items) \# \['alpha', 'beta'\] 2 \[alpha, beta\] === Capture by copy: var Use var when the closure needs its own independent copy of a value. Changes to the original don't affect the closure, and changes inside the closure don't affect the original. def main(): var snapshot\_val = 42 def frozen() {var snapshot\_val} -\> Int: return snapshot\_val snapshot\_val = 999 print(frozen()) \# 42 (captured the value at definition time) 42 The closure copied snapshot\_val when it was created. Later changes to snapshot\_val in the outer scope don't affect the closure's copy. The implicit form \`\` copies every used outer value: def main(): var x = 10 var y = 20 def snap() {var} -\> Int: return x + y x = 0 y = 0 print(snap()) \# 30 (uses copied values) 30 #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Copy captures call the value's copy constructor at the point where the closure is defined. For types with expensive copies (large lists, strings with allocations), prefer read or mut when you don't need an independent copy. ] === Move capture: var name^ Use var name^ to transfer ownership of a value into the closure. The closure consumes the outer binding, which can't be used after the closure is created: def main(): var data: List\[Int\] = \[1, 2, 3\] def take\_data() {var data^}: print(data) take\_data() \# \[1, 2, 3\] \# data can't be used here: ownership transferred to the closure \# print(data) \# Uncomment for error: 'data' is uninitialized after move \[1, 2, 3\] Move capture avoids a copy entirely. The value moves into the closure's storage. This is useful for types that are expensive to copy or for transferring unique ownership. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ The ^ transfer operator only works with var or a bare name in capture lists. {mut name^}, {ref name^}, and {read name^} are compiler errors. ] === Copyable closures: var^ The {var^} capture list moves all referenced outer values into the closure. When the captured types are Copyable, the closure value also becomes copyable. This allows the closure itself to be assigned to new variables or passed by value. def main(): var label = "sensor-1" def tag() {var^} -\> String: return label var also\_tag = tag \# copies the closure (and its captures) print(tag()) \# sensor-1 print(also\_tag()) \# sensor-1 sensor-1 sensor-1 #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ When you copy a closure created with {var^}, each captured value is copied again through its copy constructor. For closures that capture large or expensive values, be aware of the cost. ] Without {var^}, closures can't be assigned to new variables or copied. === Caller-determined mutability: ref Use {ref name} when the closure's mutability depends on the caller's context. If the caller provides a mutable reference, the closure captures mutably. If immutable, the closure captures immutably. The following example uses comptime if origin\_of(items).mut to inspect how the closure captures the value at each call site: def show\_mutability(ref items: List\[Int\]): def report() {ref items}: comptime if origin\_of(items).mut: print("mut") else: print("immut") report() \# Show immutability: \`xs\` uses the default \`read\` argument convention def from\_read(xs: List\[Int\]): show\_mutability(xs) \# xs is an immutable reference here \# Show mutability: \`xs\` uses the \`mut\` argument convention def from\_mut(mut xs: List\[Int\]): show\_mutability(xs) \# xs is a mutable reference here def main(): var nums: List\[Int\] = \[10, 20, 30\] from\_read(nums) \# immut from\_mut(nums) \# mut immut mut {ref name} doesn't choose a mutability. It shares the captured name's existing origin. The mutability is whatever that origin already carries, decided wherever name was bound. This is often the function's own ref parameter, ultimately resolved at the call site. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ ref captures are an advanced feature for writing generic code that works across mutability contexts. For most closures, read or mut is the right choice. ] === Empty capture list: {} An empty capture list means the closure uses nothing from its surrounding scope. It's a plain function that happens to be defined inside another function: def main(): def doubled(x: Int) {} -\> Int: return x \* 2 print(doubled(5)) \# 10 10 The body may only use its own arguments. Referencing any outer value is a compile error: \# This example doesn't compile def main(): var a = 42 def wrong() {}: print(a) \# error: no capture convention for 'a' /var/folders/vj/yj\_407r12vd0kndj7dxwxw740000gn/T/mojo-run-3vyr\_1fw/ch03-026.mojo:7:15: error: Could not infer capture convention of the captured value a print(a) \# error: no capture convention for 'a' ^ /Users/mbpro/FromOldMac/Claude/mojo-cas/.pixi/envs/default/bin/mojo: error: failed to parse the provided Mojo source module === Mixing capture conventions A capture list is a comma-separated sequence of independent entries. Each entry specifies its own convention, and conventions don't carry over from one entry to the next. def main(): var config = "prod" var count = 0 var label = "run-1" def process() {read config, mut count, var label}: count += 1 print(config, count, label) process() \# prod 1 run-1 label = "run-2" process() \# prod 2 run-1 (label was copied at definition time) prod 1 run-1 prod 2 run-1 Each entry is self-contained: read config is a read-only reference, mut count is a mutable reference, and var label is a copy. A bare name without a convention keyword defaults to read: def main(): var x = 10 def show() {x}: \# same as {read x} print(x) show() \# 10 10 ==== Setting a default convention A convention list can mix implicit and explicit entries, such as {read, mut count, var label}. You may use at most one implicit entry per capture list, and you can place the entries in any order. {mut count, var label, read} is equivalent to {var label, read, mut count}. For example: def main(): var a = 1 var b = 2 var z = "snapshot" def mixed() {mut, var z}: a += 10 b += 20 print(a, b, z) mixed() \# 11 22 snapshot z = "changed" mixed() \# 21 42 snapshot \# z was copied at def-time \# Changes to outer z don't reach the closure 11 22 snapshot 21 42 snapshot === Closures in practice ==== Configurable behavior Closures let you build specialized behavior from general-purpose parts. The following generic function accepts any callable with the expected signature. The closure carries the configuration: \# \`G\` matches any \`def(String) -\> None\` callable def greet\_all\[G: def(String) -\> None\](names: List\[String\], greet: G): for n in names: greet(n) def main(): var names: List\[String\] = \["Alice", "Bob"\] var greeting = "Hello" def greeter(name: String) {read greeting}: print(greeting + ", " + name + "!") greet\_all(names, greeter) \# Hello, Alice! \# Hello, Bob! greeting = "Hi" greet\_all(names, greeter) \# Hi, Alice! \# Hi, Bob! Hello, Alice! Hello, Bob! Hi, Alice! Hi, Bob! The inner function greeter captures greeting as an immutable read-only reference. greet\_all doesn't know anything about the greeting itself. It only knows how to call a function with the type def(String) -\> None. Because the closure captures greeting by reference instead of by copy, changes to greeting between calls are visible inside the closure. ==== Accumulating state Closures with mut captures can build up results across multiple calls. def main(): var log = List\[String\]() def record(event: String) {mut log}: log.append(event) record("started") record("processed item") record("finished") for entry in log: print(entry) \# started \# processed item \# finished started processed item finished The closure record mutates log in the outer scope. Each call appends to the same list without passing it as an argument. ==== Separating logic from execution The standard library uses closures to separate what you compute from how it gets executed. You provide the logic; the library handles parallelism, vectorization, or hardware dispatch. from std.algorithm import parallelize def main(): var results = List\[Int\](length=8, fill=0) def work(i: Int) {mut results}: results\[i\] = i \* i parallelize(work, 8) print(results) \# \[0, 1, 4, 9, 16, 25, 36, 49\] \[0, 1, 4, 9, 16, 25, 36, 49\] You define work with the logic for a single element. parallelize calls it across available threads. Each call to work accesses the same results through mutable capture. The squared values land in the outer list, ready to use after parallelize finishes.