Skip to content

Classes

A class instance is shared on assignment and can be changed through any name that refers to it; see Values & Sharing. A class that implements Pattern is a script pattern.

class Point {
let x; // field
let y: Number; // typed field: every write is checked
fn new(x, y = 0) { // constructor
this.x = x;
this.y = y;
}
fn add(other) -> Point { // method
return Point(this.x + other.x, this.y + other.y);
}
fn __str__() { // how print() and f-strings show it
return f"({this.x}, {this.y})";
}
}
let p = Point(3, 4); // call the class to construct
let q = p.add(Point(y: 2, x: 1)); // named arguments work
assert q.x == 4;
print(q); // (4, 6)
MemberSyntaxNotes
Fieldlet name; or let name: Type;Declared once; takes no /// documentation
Constructorfn new(params) { … }Called by ClassName(args); sets fields through this
Methodfn name(params) { … }Called as instance.name(args); /// documents it
Displayfn __str__() { … }Used by print() and format strings
Conversionfn from(x: Source) -> ClassName { … }Applied where the class is asked for; never called

Parameters of new and methods follow the function rules: defaults, named arguments, rest parameters, and argument-count errors. A method’s default may use this.

A field and a method may share a name: p.x reads the field and p.x() calls the method.

Fields are read and assigned from anywhere, including through nested instances, and take compound assignment:

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;
o.inner.value += 1;
assert o.inner.value == 6;
print(o.inner.value);

A method that assigns to this changes the instance in place, and every name sharing the instance sees the change. A class that implements Clone has .clone(), which makes an independent copy.

With __str__, print() and f"{…}" show what it returns. Without it, or when it returns a non-String, an instance shows as its class name and its fields in alphabetical order:

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 }
assert type(Pair(1, 2)) == "Pair";

fn from declares how a value of another type becomes a value of the class. Where an annotation asks for the class and a value does not fit it, from converts the value once, at that place: an argument, a return, a typed variable or a typed field.

class Hz {
let value: Number;
fn new(value) { this.value = value; }
fn from(n: Number) -> Hz { return Hz(n); }
}
fn period_ms(f: Hz) -> Number { return 1000 / f.value; }
assert period_ms(500) == 2; // 500 converts to Hz(500)
let a: Hz = 440;
assert a.value == 440;
  • A class has one from. It takes one annotated parameter, has no default or rest parameter, returns the class itself, and does not use this. A union parameter (x: Number | String) takes several types.
  • from is not a method: a.from(1) is an error. Write let a: Hz = 1; instead.
  • What from returns must be the class as it is. A conversion never leads to another one.
  • An interface cannot declare from.
  • A pattern callback converts too, with the same from.

How conversions apply at a boundary is in Values & Types. The prelude’s Note converts a MIDI number: chord(60, "maj").

A class in a module is exported like a fn and constructed as module.ClassName(args). private class keeps it inside the module. See Modules.

An interface names the methods that several classes have in common. A class that names it after implements defines those methods, and its values fit the interface wherever an annotation names it:

interface Shape {
fn area() -> Number; // required: each class defines it
fn twice() -> Number { // default: a class may leave it out
return 2 * this.area();
}
}
class Square implements Shape {
let side: Number;
fn new(side) { this.side = side; }
fn area() -> Number { return this.side * this.side; }
}
fn total(a: Shape, b: Shape) -> Number {
return a.area() + b.area();
}
assert total(Square(2), Square(3)) == 13;
assert Square(2).twice() == 8; // the default, with this as the square
  • An interface holds methods only: no fields, no new and no from. An interface does not implement another interface.
  • A method written with ; is required. A method with a body is a default.
  • A class implements several interfaces with a list: class Clip implements Shape, Named.
  • A class’s own method is used before a default. When two of its interfaces have a method of the same name, the class must define that method itself.
  • In a default, this is the instance. The editor completes this. with the interface’s methods.
  • An interface is a type and makes no values: a function can have the same name.
  • type(x) names the class of the value, not the interface. A builtin value that implements an interface natively is named by the interface: type([C4 E4]) is "Pattern".
  • The stdlib’s Pattern is an interface: a class that implements it is a pattern, and has every combinator. See Script Patterns.

A method of an interface may return Self: the type of the value it is called on. A class implementing it returns its own class, written by name or as Self. The prelude’s Clone is such an interface. A class that implements it defines what its copy is:

class Point implements Clone {
let x;
let y;
fn new(x, y) { this.x = x; this.y = y; }
fn clone() -> Self { return Point(this.x, this.y); }
}
let a = Point(1, 2);
let b = a.clone();
b.x = 10;
assert a.x == 1;

Arrays, dicts, tuples, sections and timelines are Clone natively. A copy clones each value inside it that is Clone, through that value’s own clone(), and shares every other value.

The class is checked when its definition runs, and the editor shows the same errors. The class must define each required method itself. Each method must have the interface’s parameter names, in the same order, with a default and a rest parameter where the interface has one. Each parameter must take the values that the interface’s parameter takes. When the interface declares a return type, the class must declare a return type that fits it:

class Blob implements Shape { }
// → Type error at 1:23: Blob implements Shape but has no area()

A class keeps the interfaces as they were when its definition ran. If you execute an interface again with changes, the classes defined before keep the old methods. The next class definition is checked against the new interface.

In a module, an interface is exported as a class is, and private interface keeps it in the module. Import it by name: use "./shapes" { Shape };.

A native class is the type of values the engine or a native extension makes: AudioTrack, Pattern, Sampler. It declares the methods of those values, and nothing else:

  • It has no fields and no new.
  • Its name is a type, not a constructor. A function of the same name, such as fn AudioTrack(name), makes the values.

The standard library declares its builtin types this way. Write one only for the values a native extension returns.

  • Inheritance — no extends or super. Classes share methods through an interface.
  • Static methods or fields — every member belongs to an instance.
  • Private members — every field and method is public. For encapsulation, use module-level private.
  • Getters and setters — fields are read and assigned directly.