Classes & References
Defining a class
Section titled “Defining a class”A class declares fields with let, a constructor with fn new(...), and methods with fn name(...).
class Counter { let count;
fn new(initial) { this.count = initial; }
fn get() { return this.count; }
fn increment() { return Counter(this.count + 1); }
fn add(n) { return Counter(this.count + n); }}Construct an instance by calling the class name as a function:
let c = Counter(0);print(c.get()); // 0Immutable by default
Section titled “Immutable by default”Methods return new instances rather than mutating in place. This makes method chaining safe and predictable.
let c1 = Counter(0);let c2 = c1.increment().increment().increment();
print(c2.get()); // 3print(c1.get()); // 0 (unchanged)Fields, constructor, and methods
Section titled “Fields, constructor, and methods”The Point example
Section titled “The Point example”class Point { let x; let y;
fn new(x_val, y_val) { this.x = x_val; this.y = y_val; }
fn add(other) { return Point(this.x + other.x, this.y + other.y); }
fn scale(factor) { return Point(this.x * factor, this.y * factor); }
fn distance_squared() { return this.x * this.x + this.y * this.y; }}
let p1 = Point(3, 4);print(p1.x); // 3print(p1.distance_squared()); // 25
let p2 = Point(1, 2);let p3 = p1.add(p2);print(p3.x); // 4print(p3.y); // 6External field assignment
Section titled “External field assignment”Fields can be set directly from outside the class:
let origin = Point(0, 0);origin.x = 42;print(origin.x); // 42Compound assignment
Section titled “Compound assignment”Compound operators work on fields:
let p = Point(10, 20);p.x += 5;print(p.x); // 15Type annotations
Section titled “Type annotations”Fields and method signatures can carry type annotations.
class Velocity { let speed: Number; let direction: String;
fn new(speed: Number, direction: String) { this.speed = speed; this.direction = direction; }
fn scale(factor: Number) -> Velocity { return Velocity(this.speed * factor, this.direction); }
fn __str__() -> String { return f"{this.speed} {this.direction}"; }}
let v = Velocity(120, "north");print(v.scale(0.5)); // 60 northOptional parameters and named arguments
Section titled “Optional parameters and named arguments”Constructor and method parameters follow the same rules as functions: a parameter with a = default is optional, and arguments can be passed by name. Omitting an optional argument evaluates its default; a default may reference this or an earlier parameter. Passing too few (or too many) arguments is an error.
class Note2 { let pitch; let velocity;
// `velocity` is optional; omitting it uses the default. fn new(pitch, velocity = 100) { this.pitch = pitch; this.velocity = velocity; }
// A method default may reference an earlier parameter. fn transposed(semitones, keep_velocity = this.velocity) { return Note2(this.pitch + semitones, keep_velocity); }}
let a = Note2(60); // velocity defaults to 100print(a.velocity); // 100
let b = Note2(60, 40); // velocity providedprint(b.velocity); // 40
let c = Note2(pitch: 64); // named argument, velocity defaultsprint(c.pitch); // 64
let d = a.transposed(12); // keep_velocity defaults to this.velocityprint(d.pitch); // 72Doc comments
Section titled “Doc comments”Use /// to attach documentation to class methods. Doc comments are visible via LSP hover and the show() introspection function.
class Chord { let root; let intervals;
fn new(root, intervals) { this.root = root; this.intervals = intervals; }
/// Return the notes of this chord as an array. /// Each note is the root transposed by the corresponding interval. fn notes() { return this.intervals.map(fn(i) { return this.root + i; }); }
/// Invert the chord by shifting the lowest note up an octave. fn invert() { let new_intervals = this.intervals.slice(1, this.intervals.length()) .concat(#[this.intervals[0] + 12]); return Chord(this.root, new_intervals); }}Custom display with __str__
Section titled “Custom display with __str__”Define a __str__ method to control how instances appear in print() and format strings.
class Vector { let x; let y;
fn new(x_val, y_val) { this.x = x_val; this.y = y_val; }
fn __str__() { return f"({this.x}, {this.y})"; }
fn add(other) { return Vector(this.x + other.x, this.y + other.y); }}
let v1 = Vector(3, 4);let v2 = Vector(1, 2);print(v1); // (3, 4)print(f"v1 + v2 = {v1.add(v2)}"); // v1 + v2 = (4, 6)Default display
Section titled “Default display”Without __str__, printing an instance shows the class name and fields sorted alphabetically:
class Pair { let b; let a;
fn new(a, b) { this.a = a; this.b = b; }}
print(Pair(1, 2)); // Pair { a: 1, b: 2 }__str__ must return a string
Section titled “__str__ must return a string”If __str__ returns a non-string value, the runtime falls back to the default display format.
Share-by-default
Section titled “Share-by-default”Assignment shares the same instance. Both variables see the same data.
let p1 = Point(10, 20);let p2 = p1;p1.x = 99;
print(p1.x); // 99print(p2.x); // 99 — both see the changeTo create an independent copy, use .clone():
let p1 = Point(10, 20);let p2 = p1.clone();p1.x = 99;
print(p1.x); // 99print(p2.x); // 10 — independent copyMutable classes
Section titled “Mutable classes”Methods that modify this mutate in place. All variables sharing the same instance see the change.
class Counter { let count;
fn new(initial) { this.count = initial; }
fn get() { return this.count; }
fn increment() { this.count += 1; }}
let counter = Counter(0);let alias = counter;
alias.increment();print(counter.get()); // 1Nested fields
Section titled “Nested fields”Assignment reaches through nested instances. Because the inner instance is shared, the write is visible everywhere it is referenced.
class Inner { let value; fn new(v) { this.value = v; }}
class Outer { let inner; fn new(i) { this.inner = i; }}
let o = Outer(Inner(1));
o.inner.value = 5; // mutates the shared inner instanceprint(o.inner.value); // 5
o.inner.value += 1; // compound assignment works tooprint(o.inner.value); // 6
let alias = o;alias.inner.value = 42;print(o.inner.value); // 42 — seen through every aliasType checking
Section titled “Type checking”Use the type() function to inspect the type of any value.
print(type(42)); // "Number"print(type("hello")); // "String"print(type(true)); // "Boolean"print(type([C4 D4])); // "Pattern"print(type(Vector(1,2))); // "Vector"Practical examples
Section titled “Practical examples”Music abstractions
Section titled “Music abstractions”class Scale { let root; let intervals;
fn new(root, intervals) { this.root = root; this.intervals = intervals; }
/// Return the note at a scale degree (0-indexed). /// Adding intervals to a Note transposes it, so the result is a Note. fn degree(n) { let step = n % this.intervals.length(); let octave = (n - step) / this.intervals.length(); return this.root + this.intervals[step] + octave * 12; }
fn __str__() { return f"Scale(root={this.root}, steps={this.intervals})"; }}
let c_minor = Scale(C4, #[0, 2, 3, 5, 7, 8, 10]);print(c_minor.degree(0)); // Note: C4 (60)print(c_minor.degree(4)); // Note: G4 (67)class DrumHit { let name; let note; let velocity;
fn new(name, note, velocity) { this.name = name; this.note = note; this.velocity = velocity; }
/// Return a louder version of this hit. fn harder() { return DrumHit(this.name, this.note, min(this.velocity + 20, 127)); }
/// Return a quieter version of this hit. fn softer() { return DrumHit(this.name, this.note, max(this.velocity - 40, 1)); }
fn __str__() { return f"{this.name}(v{this.velocity})"; }}
let kick = DrumHit("kick", 36, 100);print(kick.harder()); // kick(v120)print(kick.softer()); // kick(v60)Instances in collections
Section titled “Instances in collections”Class instances work in arrays just like any other value:
let kit = #[ DrumHit("kick", 36, 100), DrumHit("snare", 38, 90), DrumHit("hat", 42, 70),];
for hit in kit { print(hit);}// kick(v100)// snare(v90)// hat(v70)class Stack { let items;
fn new(initial) { this.items = initial; }
fn push(item) { return Stack(this.items.concat(#[item])); }
fn pop() { return Stack(this.items.slice(0, this.items.length() - 1)); }
fn peek() { return this.items[this.items.length() - 1]; }
fn size() { return this.items.length(); }}
let stack = Stack(#[]);stack = stack.push(10).push(20).push(30);print(stack.size()); // 3print(stack.peek()); // 30
stack = stack.pop();print(stack.peek()); // 20Classes in modules
Section titled “Classes in modules”Classes can be exported from modules just like functions and variables. Importers instantiate them via module.ClassName(args).
class Circle { let radius;
fn new(r) { this.radius = r; }
fn area() { return 3.14159 * this.radius * this.radius; }}
private class InternalHelper { let data;}use "./shapes";
let c = shapes.Circle(5);print(c.area()); // 78.53975Mark a class private to keep it internal to the module — the same visibility rules as let and fn apply. See Modules for details.
Classes as patterns
Section titled “Classes as patterns”A class with a query(cycle) method can be used as a pattern. Assign the instance to a track and Resonon calls query() each cycle:
use "std/instruments" { SamplerMelodic, Kit };
class Stepper { let notes; let idx;
fn new(notes) { this.notes = notes; this.idx = 0; }
fn query(cycle) { let note = this.notes[this.idx % this.notes.length()]; this.idx = this.idx + 1; return note; }}
let lead = AudioTrack("lead");lead.load_instrument(SamplerMelodic(Kit("keys")));lead >> master;lead << Stepper(#[C4, D4, E4]);PLAY;State mutations (this.idx = ...) persist across cycles. See Script Patterns for full details.
Limitations
Section titled “Limitations”Next Steps
Section titled “Next Steps”With classes covered, round out the core types with text handling.
- Strings — construction, formatting, and string methods
- Script Patterns — use classes to build stateful patterns