Skip to content

Traits & Extend

trait describes capabilities a type must provide. Use traits to constrain generics and organize shared APIs. A trait is not a normal value type.

Empty traits can be declared with a semicolon:

trait Numeric;

Traits with required methods use a body:

trait Equatable {
    Bool equal(Self& lhs, Self& rhs);
}

trait Hasher {
    Void write(Self&! self, UInt8[]& bytes);
    UInt64 finish(self);
}

trait Hashable: Equatable {
    Void hash<H: Hasher>(self, H&! hasher);
}

Requirements with a self parameter use the current value as the receiver. Requirements without self are associated functions on the implementing type.

Hashable feeds a value into any concrete Hasher; it does not choose or return a hash algorithm itself. Hasher also provides write overloads with default implementations for primitive values.

Dynamic trait values use compiler-provided companion types on every trait:

Writer.Any& writer;
Writer.Receiver receiver;

Any and Receiver are reserved companion type names on traits; they are not user-defined associated types. Writer.Any& is a borrowed dynamic view of any value whose concrete type implements Writer. Construct it with Writer$.ref(value):

Writer.Any& writer = Writer$.ref(file);

The construction requires typeof(file): Writer; it is not an unchecked cast and it does not move file. Use Writer$.new(value) when the dynamic trait value should own the receiver:

Writer.Any^ writer = Writer$.new(file);

An owning dynamic trait value stores the receiver and drops it when the Writer.Any^ is dropped. The compiler keeps a method table for the concrete implementation internally; that table type is not a public type user source can name. Writer.Receiver is the erased receiver used by instance-method slots:

Writer.Receiver receiver = writer.receiver;
writer.write(123);

Writer.Any& conceptually stores a Writer.Receiver plus the matching method table. Calls through Writer.Any& dispatch through that table:

Void write_one(Writer.Any& writer) {
    writer.write(123);
}

Passing a trait method pointer uses the unbound vtable slot form. Receiver methods include the erased receiver as their first parameter, while associated functions keep their declared parameter list:

RawFn<Void, Writer.Receiver, Int> write_slot = writer.write;
write_slot(writer.receiver, 123);

The current implementation supports dynamic dispatch for reference receiver methods, method slot access, and dropping owning dynamic trait values.

Types can list traits in their declaration and provide matching methods:

trait HasValue {
    Int value(self);
}

struct Box: HasValue {
    Int inner;

    Int value(self) {
        return self.inner;
    }
}

extend adds methods or a trait implementation after the type declaration:

struct User {
    Int id;
}

extend User {
    Int id_value(self) {
        return self.id;
    }
}

extend User: HasValue {
    Int value(self) {
        return self.id;
    }
}

extend Type: Trait { ... } provides that trait’s required methods for the type. Generic extensions, @where type-shape matching, module visibility, and public extend are covered in Extensions.

trait Iterator {
    associated Element;
    Element? next(self);
}

Associated types can have bounds:

trait HasItem {
    associated Element: Hashable & Equatable;
    Element item(self);
}

Implementations bind associated types:

struct Counter: Iterator {
    associated Element = UInt8;

    UInt8? next(self) {
        return null;
    }
}

extend can bind them too:

struct RangeCounter {}

extend RangeCounter: Iterator {
    associated Element = Int;

    Int? next(self) {
        return null;
    }
}

Use a qualified associated type path when multiple traits expose the same associated name:

trait Left {
    associated Item;
    Item left(self);
}

trait Right {
    associated Item;
    Item right(self);
}

struct Pair: Left, Right {
    associated Left.Item = UInt8;
    associated Right.Item = Int;

    UInt8 left(self) {
        return 20;
    }

    Int right(self) {
        return 22;
    }
}
@where(T: Hashable & Equatable)
UInt hash_key<T>(T value);

Associated type binding:

@where(T: Sequence<Element = UInt8>)
Int count<T>(T value);

Projected equality:

@where(T.[Sequence].Element == UInt8)
Int count_bytes<T>(T value);

The prelude provides common trait and bound names such as:

  • Numeric
  • Mutable
  • Hashable
  • Equatable
  • FromStringLiteral
  • Iterator / Sequence
  • SubscriptGet / SubscriptSet

Subscript traits can model value[index]:

public trait SubscriptGet {
    associated Index: Equatable;
    associated Value;

    Value subscript_get(self, Index index);
}

public trait SubscriptSet: SubscriptGet {
    Void subscript_set(self, Index index, Value value);
}