Classes

Classes

[public] class Counter {
    count: i64
    label: string

    /// A counter that starts at zero.
    constructor(label: string) {
        this.count = constant<i64>(0)
        this.label = label
    }

    fn size() => number {
        return new number(danger_cast<f64>(this.count))
    }
}

Fields

Fields are declared without let and always state their type. The constructor has to initialize every one of them.

Constructors

constructor runs at new:

let counter = new Counter("requests")

A class can declare more than one constructor when the parameter lists differ.

Methods and this

Methods are declared with fn inside the class body and reach the instance through this.

A method that assigns to an attribute of this is a mutating method. The compiler works this out on its own, so [mutates] is inferred and does not have to be written. Writing it is allowed when the intent is worth stating outright:

class Holder {
    value: string?

    constructor() {
        this.value = None
    }

    [mutates] fn set(next: string) {
        this.value = next
    }
}

Inference also follows calls: a method that calls a mutating method on this is itself mutating. This matters because mutation is what drives the state system, which the state page covers.

Inheritance

from declares a base class:

[public] class JsonString from JsonValue {
    // ...
}

A subclass inherits the base's fields and methods. Inheritance is single: a class has at most one base, though it can implement any number of traits.

Every class ultimately inherits from the root object type, which is where to_string, hash, and on_state_changed come from. That is why any value can be printed or used as a map key without the class doing anything.

Calling the base constructor

A subclass constructor calls the base constructor with => after its parameter list, before the body:

[public] class JsonString from JsonValue {
    text: string

    constructor(text: string) => super() {
        this.text = text
    }
}

Arguments pass through to the base:

constructor(name: string, size: number) => super(name) {
    this.size = size
}

The call belongs to the declaration rather than the body, so it runs before the body does and the base is fully initialized by the time the body starts. A class with no base cannot use it: a super call without a parent class is an error.

super in methods

super also refers to the base class from inside a subclass method. It is what lets an override extend the base behavior instead of replacing it:

[public] class JsonString from JsonValue {
    fn to_string() => string {
        return `"${super.to_string()}"`
    }
}

Calling super.to_string() runs the base implementation of that method rather than the override, which would otherwise call itself.

super reaches any inherited member, not only the one being overridden:

class Document from Node {
    fn describe() => string {
        return `${super.name()} (${super.kind()})`
    }
}

The distinction from this matters when a method is overridden. this.method() dispatches to the most derived implementation, so inside an override it calls the override again. super.method() always means the base's version, one level up the chain.

Where a base method is being wrapped rather than replaced, calling super first and then adding to the result is the usual shape, since the base half of the work still has to happen.

Implementing traits

impl lists the traits a class satisfies, separated by commas:

[public] class Array<T> impl Index<number, T>, IndexRef<number, T> {
    // ...
}

A class may both inherit and implement, with from first.