The object model

The object model

Every class with no declared parent inherits Object, so a handful of methods exist on every value in the language.

[public] class Object impl Hash {
    [public] fn to_string() => string
    [public] fn to_number() => number?
    [public] fn to_bool() => bool?
    [public] fn to_char() => char?
    [public] fn on_state_changed(name: string)
    [public] fn hash() => number
}

Three things follow from this.

The conversions return optionals. to_number, to_bool, and to_char default to None, because not every value has a meaningful rendering. Only to_string returns a plain value, defaulting to the class name.

Everything is hashable, but not usefully. Object implements Hash with a constant zero. That means any class satisfies an impl Hash bound without doing anything, and a class used as a Map key without overriding hash() puts every entry in one bucket. Override it for a key type.

Object does not implement Equals. Unlike Hash, equality has no default, so a key type has to define it. See the Map keys page.

on_state_changed

on_state_changed is the hook behind the [state] modifier. Override it to react when the object's own data changes:

class Document {
    [state] title: string

    constructor() {
        this.title = ""
    }

    fn on_state_changed(name: string) {
        io::println(`${name} changed`)
    }
}

It fires on assignment to a [state] attribute and on a call to a method that mutates one. It does not fire inside a constructor, since the object is not finished being built.

Box

Generics are erased, which means a type parameter is only usable through its bounds and a raw machine value cannot be carried in one directly. Box<T> is the way around that:

[public] class Box<T> {
    constructor(value: T)
    [public] fn get() => T
    [mutates] fn set(value: T)
}

Box is the one generic the compiler specializes per type, so it can hold a raw FFI value where an erased generic cannot: a machine scalar, an opaque handle, a pointer<T>.

let handles: Array<Box<opaque>> = new Array<Box<opaque>>()
handles.push(new Box<opaque>(native_handle))

let first: opaque = handles[0].get()

Reach for it when you want a collection of raw values, or when a generic API needs to carry something the collector does not manage. For ordinary objects it is unnecessary, since those pass through generics unchanged.

The value types

number, bool, char, and string are ordinary classes wrapping a raw machine scalar. That is why 1 + 2 routes through a trait method while raw scalar arithmetic lowers straight to machine code.

Class Wraps Notes
number f64 The general numeric type. to_string gives the shortest round-tripping decimal.
bool i1
char i8 One byte. The is_* tests are ASCII.
string pointer<i8> + i64 Byte-indexed, not character-indexed.

Boxing is one-directional: a raw scalar is promoted to its wrapper automatically, but a wrapper is never silently treated as a raw scalar. Unbox explicitly with .to_raw().