Skip to content

Names & Live Editing

In a live session you don’t run a program once. You select a line here and a block there, in any order, while the music plays (see The Evaluation Model). The rules for names are built for that: a name means what you last executed for it, and what a piece of code means depends on what it says and on the session’s current state — never on who called it.

The session has one namespace for everything you bind at the top level: lets, fn, class and dsp definitions, and the names a use brings in. There is no ranking between them. Execute fn Delay(…), then use "std/effects" { Delay };, and Delay is the effect; execute the fn again and it is the function again. A call and a plain mention of the name always agree, because they look in the same place.

That is the only rule you need while performing: to know what a name means, remember what you ran last. A hidden precedence — definitions over imports, say — would make the line you just executed silently do nothing.

A function sees the session, not its caller

Section titled “A function sees the session, not its caller”

A named fn looks up any name that isn’t its own parameter or local in the session, when it is called. A caller’s local variables never leak into it, or into a class method:

let tempo = 120;
fn beat_ms() {
return 60000 / tempo;
}
fn play(tempo) { // this tempo is local to play
return beat_ms(); // beat_ms still reads the session's tempo
}
assert play(999) == 500;
print(beat_ms());

This keeps each function readable on its own: to know what beat_ms does, read its body and the session names it uses. If lookups followed the call chain instead, the same line would do different things depending on which function you selected and ran it from — exactly the uncertainty select-and-execute cannot afford.

Because the lookup happens at call time, changing a session name changes every function that reads it. You don’t redefine the functions:

let tempo = 120;
fn beat_ms() { return 60000 / tempo; }
tempo = 150;
assert beat_ms() == 400; // picks up the new value
print("live");

This is what live coding leans on. Tweak a value, run that one line, and the instrument functions built on it follow. Redefining a function works the same way: every function that calls it gets the new version from its next call.

Sometimes you want the opposite: a function that remembers a value as it was. That is what a closure — a fn(…) { … } value — is for. It captures the variables it refers to, by value, at the moment it is created:

fn make_adder(n) {
return fn(x) { return x + n; }; // n is captured now
}
let add5 = make_adder(5);
assert add5(3) == 8;
print("frozen");

What a closure freezes is the values it reads. A function it calls is looked up when the closure runs, as in a named fn, so redefining that function reaches closures that already exist:

let f = fn() { return g(); };
fn g() { return 1; }
assert f() == 1;
print("late");

Run fn g() { return 2; } later in the session and f() returns 2. Reach for a named function or a session value when behaviour should follow the session, and for a closure’s captured values when it should hold on to one. The same idea is why pattern generators freeze what they capture: a stream that changed when an unrelated variable did could not replay the same cycle twice.

A function you give a pattern — p.map(fn(n) { … }), .map_with_timing(…), or a class’s query(cycle) — is a closure like any other. The values it reads are frozen when the pattern is made. The functions it calls are looked up each time the pattern computes a cycle:

let melody = [C4 E4 G4].map(fn(n) { return lift(n); });
fn lift(n) { return n + 12; }
print("follows");

While melody plays, run fn lift(n) { return n + 7; } and it plays a fifth up instead, from the next cycle it computes after that line has run. You don’t re-run the line that made the pattern.

So a playing pattern can also break: run a lift that calls a function that doesn’t exist, and the callback fails each time it runs. A failing map callback plays its notes unchanged, and a failing query rests. Either way the error is printed, once per run however often it fails, and printed again after your next run if it is still failing:

A playing pattern's map callback failed: Undefined function 'h' at song.non:1:21
|
1 | fn lift(n) { return h(n); }
| ^^^^
in the map callback:
|
4 | let melody = [C4 E4 G4].map(fn(n) { return lift(n); });
| ^^^^^^^^^^^^^^^^^^^

It quotes the code as it was run, because by the time a pattern computes a cycle you may have moved those lines. For the same reason the error is not underlined in the editor.

Within one piece of source you run, a name has one definition. Two fn greets, a fn tempo beside a let tempo, a use and a dsp effect both binding Delay, or two methods of one name in a class are an error, and nothing in that source runs. Arity never chooses between functions; a parameter that may be left out gets a default value.

The reason is the same predictability. If tempo could be both a value and a function, or greet could mean two bodies depending on how many arguments you passed, then reading a line would not tell you what it does. Refusing the whole source, rather than running up to the clash, means an execution never half-applies.

Running a binding again later is different, and it is the point of live coding: execute a new fn greet and it replaces the old one from the next call on, while everything else keeps playing. The rule is about one piece of source contradicting itself, not about changing your mind.

The editor follows the same line. Until you evaluate anything, it is in File Mode (offline): the file is the piece of source, so a name it defines twice is marked. From your first evaluation it is in Live Mode (online): the file is where you write selections, and a second fn greet further down is the next version of greet, not a mistake. The status bar shows the mode; see Live Mode and File Mode.

fn, class and dsp definitions sit at the top level. A definition names something for the whole file, so it is written where the whole file can see it; a helper that belongs to one place is a closure bound with let. A definition exists from the line that defines it, so a call written above a fn fails as undefined — unless an earlier run already defined it, which is the session remembering.

The standard library is a layer below the session. Any of its names can be bound in the session — fn clamp, say — and from then on your code gets yours. The standard library itself never sees the session: its functions keep calling each other, so let round = 0; cannot break a function that rounds inside. Overriding a name is a decision about your code, and only about your code.

A module’s code sees its own top-level names and the standard library, never the session that imports it. A library behaves the same in every piece that uses it, and a let you happen to write in your piece cannot change what it does. What a module needs from you, it takes as arguments.

A use evaluates a module when its source, or the source of anything it imports, differs from what the session last evaluated. So you edit a library file, run your use line again, and the new definitions are there — there is no reload gesture to remember. And when nothing changed, nothing runs again: a module that builds a track at its top level builds it once, not on every unrelated execution. Freshness follows the imports, so editing a module that another one imports refreshes both.

RESET; is the full reload, for when you want everything evaluated from scratch.