Skip to content

Values & Sharing

A few choices about values shape how resonon code behaves: which values are shared, how strict the types are, and what an annotation promises. Each follows from the same aim — code you can select and run in any order, without being surprised by what it does.

Assigning a container — an array, a dict or a class instance — does not copy it. Both names then refer to the same data, and a change through one is seen through the other. Passing a container to a function works the same way:

let a = #[1, 2, 3];
let b = a;
b.push(99);
assert a.length() == 4; // a sees the change
fn append_high(arr) {
arr.push(C5);
}
let melody = #[C4, E4, G4];
append_high(melody);
assert melody.length() == 4; // the caller's array changed
print(melody);

This is the rule the rest of a session relies on. Tracks, instruments, effects, MIDI tracks and visuals are shared handles too: let lead = drums; gives the same track a second name, it does not build a second track. A DAW works the same way — a track is one object however many places refer to it — and if assignment copied, a helper function that configured a track would configure a copy that nobody hears.

When you do want an independent copy, say so with .clone():

let a = #[1, 2, 3];
let b = a.clone();
b.push(99);
assert a.length() == 3; // unchanged
print(b);

Primitives — Numbers, Strings, Booleans, Notes and NUL — are always copied, so there is nothing to share. Strings and tuples cannot be changed at all: every string method returns a new string, and a tuple has no push or element assignment. A value that cannot change behaves the same whether it is shared or copied.

Changing in place, or returning a new value

Section titled “Changing in place, or returning a new value”

A class method can change this, or it can return a new instance and leave this alone:

class Counter {
let count;
fn new(n) { this.count = n; }
fn increment() { this.count += 1; } // changes in place
fn incremented() { return Counter(this.count + 1); } // returns a new one
}
let c = Counter(0);
let alias = c;
c.increment();
assert alias.count == 1; // the alias sees the change
let d = c.incremented().incremented();
assert c.count == 1; // c is untouched
print(d.count); // 3

Both are legitimate; the class you write decides. Patterns always take the second form: every pattern method returns a new pattern, which is why one motif can feed any number of variations (see How Patterns Work). Returning new values makes chains like c.incremented().incremented() safe to write, because no link in the chain changes something another name is holding.

Resonon does not quietly convert one type into another. 5 + "5" concatenates because one side is a String, but C4 + C4, true && 0 or NUL + 1 are type errors, not a best guess. The cross-type rules that exist are few and each has a musical reason: a Note compares with a Number by MIDI value (C4 == 60), + and - transpose a pitch, and * and / scale its frequency. They are listed in the type-compatibility table.

The same thinking separates conditions from truthiness. An if, a match guard or an assert needs a real Boolean, so if count { } is an error rather than a silent test for zero — you write what you mean, if count > 0. Only where a callback’s answer decides whether to keep an element (.filter(), .find(), .any(), .all()) does truthiness apply, and there only false and NUL are falsy. 0, "" and #[] are real values, and treating them as “nothing” is a classic source of dropped notes.

NUL means “no value”, and it stays that way. It does not propagate through arithmetic the way SQL’s NULL does: using it as an operand is an error at the line that did it, not a NUL that surfaces three functions later. Reading a variable that was never defined is an error too, so a typo shows up where you made it.

Annotations document, they don’t enforce

Section titled “Annotations document, they don’t enforce”

A type annotation (fn f(x: Number) -> Number) is a contract for readers and for the editor, which uses it for signatures, hover and completion. It is never checked at run time: resonon is dynamically typed on purpose, so that a value can be whatever the moment needs while you are performing.

Annotations earn their place where a shape is fixed — function parameters and returns, class fields. A variable has no fixed type (its type is whatever it holds now), so an annotation on a let describes nothing the editor doesn’t already know from the value; by convention it is left out. Where the two disagree, the editor shows the value’s real type.