← Back to AILP Home

Basics — trait, impl, UFCS

A trait declares a named set of method signatures. An impl T:for:U block supplies bodies for those signatures for a concrete type U.

Declaring a trait

trait:Bumpable = {
    func:bump = int32($$m Bumpable:self);
};

Inside the trait body:

Writing an impl

struct:Counter = { int32:n; };

impl:Bumpable:for:Counter = {
    func:bump = int32($$m Counter:self) {
        self.n = self.n + 1i32;
        pass self.n;
    };
};

Rules:

Calling a trait method (UFCS, static dispatch)

func:failsafe = int32(tbb32:err) { exit 1; };

func:main = int32() {
    Counter:c = { n: 7i32 };
    int32:r = c.bump();   // → 8, c.n is now 8
    exit r;
};

When the receiver's type is known at compile time (here Counter), the call resolves directly to Counter::bump — no vtable, no fat pointer. This is the cheapest call shape and the one to prefer when the type is statically known.

What about failsafe?

Every Nitpick program must declare failsafe — the runtime calls it if any tbb32-tagged error propagates out of main. It is not a trait concept, but every example in this cookbook includes it so the fixtures compile standalone.

Worked example (bug372)

struct:Counter = { int32:n; };

trait:Bumpable = {
    func:bump = int32($$m Bumpable:self);
};

impl:Bumpable:for:Counter = {
    func:bump = int32($$m Counter:self) {
        self.n = self.n + 1i32;
        pass self.n;
    };
};

func:failsafe = int32(tbb32:err) { exit 1; };

func:main = int32() {
    Counter:c = { n: 5i32 };
    int32:r = c.bump();
    exit c.n;            // exits 6
};

c.bump() is resolved statically to Counter::bump. The $$m receiver makes mutation through self.n visible to the caller — c.n is 6 on return.

Common diagnostics