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 {
() write(Self&! self, UInt8[]& bytes);
UInt64 finish(self);
}
trait Hashable: Equatable {
() 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:

() 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<(), 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 {
() subscript_set(self, Index index, Value value);
}