Variables & Scope
Why names resolve the way they do, and what that means while music is playing, is in Names & Live Editing.
Declaring and assigning
Section titled “Declaring and assigning”| Form | Meaning |
|---|---|
let x = value; | Declare x in the current scope (or redefine it) |
let x; | Declare x with the value NUL |
x = value; | Assign to an existing x; any type |
x += value; | Compound assignment — see Operators |
let (a, b) = tuple; | Destructure a tuple |
private let x = value; | A module-level binding not exported — see Modules |
A name can’t be a keyword: let if = 1; is a parse error. See
Keywords.
let x = 10;x = "now a string"; // a variable takes any typelet count = 1;count += 1;assert count == 2;let later;assert later == NUL;Destructuring
Section titled “Destructuring”A destructuring let unpacks a tuple into names. _ discards an element, and patterns nest. The
shape must match: a non-tuple, or a tuple of another length, is a runtime error.
let (x, y) = (3, 4);let (first, _) = (10, 20);let (a, (b, c)) = (1, (2, 3));assert x + y + first + b == 19;Block scope
Section titled “Block scope”A let inside { … } — a block, a loop body, a function body, a match arm — is local to it and may
shadow an outer name:
let x = "outer";{ let x = "inner"; assert x == "inner";}assert x == "outer";print(x);One definition per name
Section titled “One definition per name”Within one piece of source — one file, or one selection you execute — a name has one definition.
- A
fn,classordspdefinition may not repeat a name. Arity does not distinguish functions; use default values for optional parameters. - The same holds inside a definition: one definition per method or field name in a class, and per
helper
fnin adspblock. A field and a method may share a name. - A top-level
letclaims its name too:let tempo = 120;besidefn tempo()is an error in either order. - A name a
usebinds is claimed the same way, by a secondusetoo:use "std/effects" { Delay };besidedsp effect Delayis an error. Rename the import withas. - A
letmay repeat alet. Aletinside a function or block is its own.
A repeated definition is reported where it is written, and nothing in that source runs:
fn greet() { print("hi"); }fn greet(name) { print(f"hi {name}"); } // error: fn 'greet' repeats the fn at 1:1A later run may bind the name again, with any kind of binding: a fn replaces a let or an
import, a use replaces a fn. The binding executed last wins, from the next line or call on.
Definitions live at the top level
Section titled “Definitions live at the top level”fn, class and dsp definitions are written at the top level of a file. Inside a block or a
function they are an error; bind a closure with let instead.
A definition exists from the line that defines it: a call written above its fn fails as undefined
unless an earlier run defined it.
Name resolution
Section titled “Name resolution”The session has one namespace: every top-level let, fn, class and dsp definition, and every
name a use binds, executed last wins. Below it is the standard library.
| Where a name is used | Resolves against |
|---|---|
| At the top level | The session, then the standard library |
Inside a fn body or a class method, a name that is not a parameter or local | The session, at call time, then the standard library — never the caller’s locals |
Inside a closure (fn(…) { … } value), a name it reads | The value, captured when the closure is created |
| Inside a closure, a name it calls | The session, when the closure runs, as in a fn body |
Inside a pattern callback (map, map_with_timing, a class’s query) | As in a closure: a name it reads is captured when the pattern is made, a name it calls is looked up each time the pattern computes a cycle |
| Inside a module | The module’s own names, then the standard library — see Modules |
let tempo = 120;
fn beat_ms() { return 60000 / tempo; // the session's tempo, looked up at each call}
fn play(tempo) { // a parameter, local to play return beat_ms(); // still reads the session's tempo}
assert play(999) == 500;tempo = 150;assert beat_ms() == 400; // session lookups are live
let base = 100;let get_base = fn() { return base; };base = 999;assert get_base() == 100; // the closure captured 100
let twice = fn(x) { return double(x); };fn double(x) { return x * 2; }assert twice(4) == 8; // a called name is looked up when the closure runsprint("resolved");A closure captures only the variables it reads. A fn may refer to a name defined later in the
file, as long as that name exists when the function is called.
Assignment changes the binding a name has, whichever kind it is: after fn f() {}, f = 5; makes
f the number 5. A name only the standard library defines can’t be assigned; bind your own with
let or fn.
Shadowing the standard library
Section titled “Shadowing the standard library”Any standard-library name can be bound in the session, and from then on your code gets yours. The standard library’s own functions keep resolving within the standard library, so your binding cannot break them:
fn clamp(x, lo, hi) { return x; } // your clamp, for your codeassert clamp(5, 0, 1) == 5;
let print = "not a function";put("put still prints\n"); // put calls the standard library's print