Skip to content

Values & Types

Resonon is gradually typed: you never have to write a type, and an annotation you do write is a runtime contract. An untyped variable can hold any value; a typed variable checks every write. Operators on values are in Operators; collections are in Collections & Iterators. Why containers share and primitives copy is in Values & Sharing.

SyntaxKind
// …Line comment
/* … */Block comment
/// …Doc comment for the definition below it
//! …Doc comment for the file; only at the top of the file
//// …Line comment (four or more slashes)
//! File documentation — what this file is.
// Line comment
/* Block
comment */
/// Doubles a number.
fn double(x) {
return x * 2;
}
print(double(4));

Doc comments are shown by show(), editor hover, and resonon pkg inspect.

  • /// documents the next fn, let, class, class method, interface, interface method, enum, enum variant or dsp definition. Blank lines and ordinary comments in between change nothing; consecutive /// blocks join into one.
  • A /// with nothing below it to document — above a use, an expression, a class field, inside a block, or at the end of the file — is an error.
  • A //! anywhere but the top of the file is an error.
  • A doc comment with an Experimental: line marks what it documents as experimental, with the text after it saying what may change: show(), editor hover and completion say so. In a file’s //!, it marks everything the file defines.

Use a short summary, describe the arguments and result, and include a working example. Put the types in the declaration; a type written only in a comment does not guard a value. This template uses the same section headers as the standard library:

/// Transposes a note by a number of semitones, or keeps an absent note absent.
///
/// Parameters:
/// root: Note? — the note, or NUL for no note
/// steps: Number — semitones to transpose; defaults to one octave
/// Returns:
/// Note? — the transposed note, or NUL when root is absent
/// Examples:
/// transpose_note(C4, 7)
/// transpose_note(NUL)
fn transpose_note(root: Note?, steps: Number = 12) -> Note? {
if root == NUL { return NUL; }
return root + steps;
}
assert transpose_note(C4, 7) == G4;
assert transpose_note(NUL) == NUL;

Keep comment types identical to the declaration, including ?, unions and collection element types. Use Parameters: for arguments, Returns: for the result and Example: or Examples: for examples. Visual operators also use Uniforms: and Inputs: for controls and texture inputs; these are separate from their constructor arguments.

TypeLiterals
Number42, 3.14, .5, 5., 1e-3, 2.5e2, 0xFF, 0b1010, 0xFF_FF, 0b1010_0011
String"text", f"text {expr}"
Booleantrue, false
NULNUL
NoteC4, F#3, Bb2, C-1 … G9
Rest_
Pattern[C4 E4 G4], <C4 E4>, {C4 D4 E4} — see Mini-Notation Grammar
Array#[1, 2, 3]
Dict#{"key": 1, 36: "kick"}
Tuple(1, "a")
Functionfn(x) { return x * 2; }

There is one Number type: 5 and 5.0 are the same value. Hex and binary literals accept _ separators.

Strings are double-quoted and immutable; every string method returns a new string. The methods are in the String reference.

EscapeCharacter
\"Double quote
\\Backslash
\nNewline
\rCarriage return
\tTab

An f prefix makes a format string: any expression inside {…} is evaluated and formatted as print() would show it. {{ and }} are literal braces. String literals may appear inside the braces.

let vel = 100;
let notes = #["C4", "E4", "G4"];
print(f"Dynamics: {if vel > 80 { "loud" } else { "soft" }}");
print(f"Root: {notes[0]}, count: {notes.length()}");
print(f"Literal braces: {{like this}}");
assert "Tempo: " + 120 == "Tempo: 120"; // + also concatenates

A string is not directly iterable: for ch in "abc" is an error; iterate "abc".iter(), which yields one-character strings.

if conditions, match guards, assert, &&, || and ! require an actual Boolean; any other value is a type error. Where a callback’s result is tested — .filter(), .find(), .any(), .all() — only false and NUL are falsy, and everything else, including 0, "" and #[], is truthy.

print(#[0, "", false, NUL, #[]].filter(fn(x) { return x; })); // [0, "", []]

NUL is the absence of a value.

  • NUL == NUL is true; NUL equals nothing else (not 0, not false).
  • NUL cannot be ordered (NUL < 1) or used in arithmetic, except that "text" + NUL gives "textNUL".
  • A function without a return returns NUL. Iterators return NUL once exhausted, and dict.get(key) returns NUL for a missing key.
  • Reading a variable that was never defined is an error, not NUL.
  • NUL can be stored in arrays, dicts and tuples.

A note literal is a letter, an optional accidental (# or b), and an octave from -1 to 9. Notes map to MIDI 0–127 (C-1 to G9, C4 = 60); a note outside that range is a parse error. Notes compare with Numbers by MIDI value. A name like Cs2 is not a note; it parses as an identifier.

assert C4 == 60;
assert A4 >= 69;
assert Bb3 == A#3;
print(C-1); // the lowest MIDI note
print(G9); // the highest

An annotation is a contract. A value is checked against it where the value crosses it, and a value that does not fit is a type error at that place. The language server uses annotations for signatures, hover, completion and diagnostics too. The spelling rules for NUL below are checked when the code is read.

WhereSyntax
Parameterfn f(x: Number)
Returnfn f() -> Number
Method returning its receiver’s typefn clone() -> Self
UnionNumber | String
OptionalNumber? — a Number or NUL
Function type(Number) -> Number
CollectionArray<Note>, Dict<String, Number>, Iterator<Event>
Grouped type((Number) -> Number)?
Class fieldlet speed: Number;
Tuple destructuringlet (f, l): (Number, String) = (440, "A4");
Variablelet x: Number = 1;
fn apply(f: (Number) -> Number, x: Number | String) -> Number {
return f(x);
}
print(apply(fn(n) { return n * 2; }, 5)); // 10
  • Arguments. Each argument, and each default a call leaves to a parameter. A rest parameter’s type is the type of each value it collects: ...notes: Note.
  • Returns. The value a return gives, and NUL when the body runs to its end.
  • Variables. let x: T makes a typed variable: its value and every later write fit T, a compound write (x += 1) and a closure’s write included. A typed parameter is a typed variable too. let (a, b): (A, B) types each name. A later let x = … without a type makes x untyped again.
  • Fields. Every write to a typed field. When an instance is made, each typed field that cannot be NUL must be set, by new.

An unannotated binding adds no type contract; operations still follow their own type rules. A pattern callback checks the same annotations, on the thread that plays it.

let tempo: Number = 120;
fn faster() { tempo += 5; }
faster();
assert tempo == 125;
let sound = C4; // no contract: the next value may have another type
sound = "kick";
assert sound == "kick";
let tempo = "fast"; // a fresh unannotated let removes the previous contract
assert tempo == "fast";
let tempo: Number = 120;
tempo = "fast"; // Type error: tempo is a Number; got the String "fast"

A class can declare how a value of another type becomes one of its values, with fn from. Where an annotation asks for the class and a value does not fit it, the class’s from converts the value once, at that place. A MIDI number converts to a Note: 60 is C4, and a fraction is microtonal.

let n: Note = 60.5; // a quarter-tone above C4
fn up(x: Note) -> Note { return x + 12; }
assert up(60) == C5;
print(chord(60, "maj"));
  • A value that fits as it is is not converted. x: Note | Number given 60 keeps the Number.
  • Nothing inside a collection converts. An Array<Note> given #[60] is a type error. Array<Note | Number> takes MIDI numbers as they are.
  • Operators do not convert, and a conversion never leads to another one.
  • When the froms of two members of a union both take a value, it is a type error that names both.
  • When from refuses a value, the error is at the place that asked for the conversion: up: x expects a Note; the Number inf does not convert to a Note: a MIDI note number is finite.

Array<T> checks every element against T. Dict<K, V> checks every key against K and every value against V; its key type is String, Number, or String | Number. Collections and tuples nest: Array<(Note, Number)> and Dict<String, Array<Number>> are valid types.

Bare Array, Dict, and Iterator mean Array<Any>, Dict<String | Number, Any>, and Iterator<Any>. Only these three builtin types take type arguments. Array<Note | Number> accepts both note literals and MIDI numbers; collection checks preserve the original elements.

let velocities: Array<Number> = #[80, 100];
let notes: Array<Note | Number> = #[C4, 64, G4];
let controls: Dict<String, Number> = #{"cutoff": 1200, "resonance": 0.3};
let steps: Iterator<Number> = velocities.iter();
assert steps.next() == 80;

Checks happen at annotated boundaries. An array, dict, or tuple is checked through its nested contents; an empty collection fits any element type. Shared collections can still change after a check, and crossing another annotated boundary checks their contents again. An Iterator<T> is checked by kind without pulling values from it; its lazy values are checked by their own annotations when they run.

Parentheses group types: ((Number) -> Number)? is an optional function, and (Number) -> (Number | String) is a function returning either type.

T? is a value of type T, or NUL. The ? applies to the whole type it ends: Note | Number? is a Note, a Number or NUL. (Number) -> Number? is a function that returns an optional Number.

  • NUL in a type is written with ?. Number | NUL is an error that names the fix, Number?.
  • Any already includes NUL, so Any? is an error. NUL? is an error too. -> NUL alone stays legal.
  • A parameter that defaults to NUL takes NUL, so its type says so: x: Number? = NUL. A parameter written x: Number = NUL is an error. x: Any = NUL and x = NUL stay legal.
fn transpose(root: Note | Number? = NUL, steps: Number = 12) -> Note | Number? {
if root == NUL { return NUL; }
return root + steps;
}
print(transpose(C4)); // C5
print(transpose()); // NUL

A name in an annotation is a type where the annotation is written:

  • Number, Boolean, NUL, Rest, Function and Any, everywhere.
  • Self, only as the whole return type of a method: fn clone() -> Self. It is the type of the value the method is called on, so #[1, 2].clone() is an Array<Number> and a class’s clone() is that class. Self?, Array<Self> and Self in any other annotation are errors.
  • The classes, interfaces and enums of the file, and the ones it imports.
  • The types of the prelude, such as String, Note, Pattern, AudioTrack and Signal.

A type of a module, such as Sampler, needs a use of its module. The editor marks a name that is not a type as unknown type.

An interface annotation accepts values of classes that explicitly declare implements for that interface. Merely defining matching methods does not implement it. For example, a script pattern says class Arp implements Pattern. See Interfaces and Script Patterns.

An enum annotation accepts only that enum’s variants, rather than a String with the same spelling:

enum Articulation { short, long }
fn gate(style: Articulation) -> Number {
return if style == Articulation.short { 0.25 } else { 0.9 };
}
assert gate(Articulation.short) == 0.25;

See Enums for matching and live editing.

type(value) returns the type’s name as a String; for a class instance, the class name; for an enum variant, the enum name.

assert type(42) == "Number";
assert type("hi") == "String";
assert type(true) == "Boolean";
assert type([C4 D4]) == "Pattern";
assert type((1, 2)) == "Tuple";
print(type(#[1]));