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.
Containers share, primitives copy
Section titled “Containers share, primitives copy”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 changedprint(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; // unchangedprint(b);A value that can be copied this way is Clone: arrays, dicts, tuples, sections and timelines, and
every class that says implements Clone and defines clone(). A copy clones each value inside it
that is Clone too, and shares the rest: an array of tracks copies the array, not the tracks.
Tracks, devices, patterns and iterators have no clone(), because two names for one track are one
track.
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 untouchedprint(d.count); // 3Both 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.
Types are strict
Section titled “Types are strict”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.
An annotation is a promise that holds
Section titled “An annotation is a promise that holds”A type annotation (fn f(x: Number) -> Number) is a contract, and it holds at every boundary it
guards: a value that does not fit is an error where it crosses, not a wrong result three functions
later. The editor reads the same annotation for signatures, hover and completion.
You never have to write types. Unannotated code stays dynamic: a name without a type can hold
whatever the moment needs while you are performing. Each operation still checks the types it
needs. Annotate where a shape is fixed — function parameters and returns, class fields — and where
a variable should keep one: let x: T checks every later write
to x.
A contract guards a boundary, not every alias of a value. If an Array<Number> is shared with
unannotated code, that code can still change its contents. Passing it to another annotated
parameter checks the contents again. The annotation does not copy or freeze the array.
A class can deliberately accept another type through a declared from conversion. For example,
a Note annotation accepts a MIDI number. Such conversions happen where a type is asked for;
operators and collection elements keep their own rules. See
Type annotations for the complete
boundary and conversion rules.
Next Steps
Section titled “Next Steps”- Values & Types — every literal and the truthiness rules
- Names & Live Editing — how names resolve in a running session