Skip to content

Traits

Declare behavior contracts — required methods, defaults, supertraits, associated types, and the prelude catalogue.

Updated View as Markdown

A trait names behavior that types can supply. Declaring one, implementing it, and consuming it through a bound is the whole loop:

trait Summary {
    fn summary(self): string
}

struct Article {
    title: string
    body: string
}

impl Summary for Article {
    fn summary(self): string {
        return "${self.title} (${self.body.len()} chars)"
    }
}

fn print_all[T](items: []T): void
where T: Summary {
    for item in items {
        println(item.summary())
    }
}

fn main(): void {
    print_all([
        Article { title: "Ports", body: "…" },
        Article { title: "Cranes", body: "……" },
    ])
}

Trait names use PascalCase; method names use snake_case. This page covers declaring a trait; Implementations covers writing the impl blocks that satisfy one.

Required methods

A method with no body is a requirement. The receiver form is part of the contract.

trait Counterish {
    // Read-only receiver.
    fn value(self): int
    // Mutating receiver — the implementation may write through `self`.
    fn bump(mut self): void
    // No receiver — a static function called through the type.
    fn zero(): Self
}

struct Tally { n: int }

impl Counterish for Tally {
    fn value(self): int { return self.n }
    fn bump(mut self): void { self.n = self.n + 1 }
    fn zero(): Tally { return Tally { n: 0 } }
}

fn main(): void {
    mut t := Tally.zero()
    t.bump()
    t.bump()
    println("${t.value()}")
}

A requirement carries its complete callable contract: receiver mutability, method type parameters, parameter types, success type, error type, and effect row. Fallible and effectful requirements are written exactly as ordinary signatures are:

error FetchError { Missing }

trait Fetch {
    fn fetch(self, key: string): string ! FetchError
}

struct MemStore { data: Map[string, string] }

impl Fetch for MemStore {
    fn fetch(self, key: string): string ! FetchError {
        match self.data.get(key) {
            Some(value) => return value
            None => error Missing
        }
    }
}

fn main(): void {
    mut data: Map[string, string] = Map.new()
    data.put("host", "localhost")
    store := MemStore { data: data }
    println(store.fetch("host").unwrap_or("?"))
    println(store.fetch("port").unwrap_or("?"))
}

Self

Self names the implementing type. It lets a requirement tie its inputs and outputs to the target without a second type parameter.

trait Merge {
    fn merge(self, other: Self): Self
    fn empty(): Self
}

struct Counts { hits: int, misses: int }

impl Merge for Counts {
    fn merge(self, other: Counts): Counts {
        return Counts { hits: self.hits + other.hits, misses: self.misses + other.misses }
    }
    fn empty(): Counts { return Counts { hits: 0, misses: 0 } }
}

fn combine[T](values: []T): T
where T: Merge {
    mut acc: T = T.empty()
    for value in values {
        acc = acc.merge(value)
    }
    return acc
}

fn main(): void {
    total := combine([
        Counts { hits: 3, misses: 1 },
        Counts { hits: 4, misses: 0 },
    ])
    println("${total.hits}/${total.misses}")
}

Inside the impl, write the concrete target name (Counts), not Self.

Generic traits

A trait may declare its own type parameters. Comparable[T] in the prelude is the canonical example; here is a smaller one.

trait Convert[T] {
    fn convert(self): T
}

struct Meters { value: float }
struct Feet { value: float }

impl Convert[Feet] for Meters {
    fn convert(self): Feet { return Feet { value: self.value * 3.28084 } }
}

fn convert_all[A, B](values: []A): []B
where A: Convert[B] {
    mut out: []B = []
    for value in values {
        out.add(value.convert())
    }
    return out
}

fn main(): void {
    feet: []Feet = convert_all([Meters { value: 1.0 }, Meters { value: 2.0 }])
    println("${feet.len()}")
}

Note the loop rather than values.map(v => v.convert()): a method that comes from a bound does not currently resolve inside a closure body. See Generics.

A trait may also require a static constructor, which is how Parse-style contracts are written:

trait Parse[T] {
    fn parse(input: string): T
}

struct Port { number: int }

impl Parse[Port] for Port {
    fn parse(input: string): Port {
        return Port { number: input.to_int() ?? 0 }
    }
}

fn main(): void {
    println("${Port.parse("8080").number}")
}

Bounds

A bound is what makes a trait’s methods callable in generic code. Inline and where forms state the same obligation.

trait Summary {
    fn summary(self): string
}

fn label[T: Summary](value: T): string {
    return value.summary()
}

fn label_pair[T, U](left: T, right: U): string
where T: Summary, U: Summary {
    return "${left.summary()} + ${right.summary()}"
}

struct Note { text: string }

impl Summary for Note {
    fn summary(self): string { return self.text }
}

fn main(): void {
    println(label(Note { text: "a" }))
    println(label_pair(Note { text: "a" }, Note { text: "b" }))
}

Only bounded methods are available in the body. A later implementation cannot retroactively justify an unbounded call:

trait Summary {
    fn summary(self): string
}

struct Note { text: string }

impl Summary for Note {
    fn summary(self): string { return self.text }
}

fn label[T](value: T): string {
    // No bound on T — rejected even though `Note` implements `Summary`.
    return value.summary()
}

Use where when several parameters interact or the inline list would bury the callable’s shape.

Default method bodies

A method with a body in the trait is a default. Implementations inherit it unless they override it, and the default may call the trait’s own requirements.

trait Shape {
    fn area(self): float
    fn name(self): string

    fn describe(self): string {
        return "${self.name()} with area ${self.area()}"
    }

    fn is_tiny(self): bool {
        return self.area() < 1.0
    }
}

struct Circle { radius: float }
struct Rect { w: float, h: float }

impl Shape for Circle {
    fn area(self): float { return 3.14159 * self.radius * self.radius }
    fn name(self): string { return "circle" }
}

impl Shape for Rect {
    fn area(self): float { return self.w * self.h }
    fn name(self): string { return "rect" }

    // Override the default.
    fn describe(self): string { return "${self.w}x${self.h} rect" }
}

fn main(): void {
    println(Circle { radius: 2.0 }.describe())
    println(Rect { w: 3.0, h: 0.2 }.describe())
    println("${Rect { w: 3.0, h: 0.2 }.is_tiny()}")
}

The default body is checked once against Self and the trait’s own requirements, not against any particular implementation. It cannot reach for a field or a method the trait does not declare.

Supertraits

Parent requirements go after :, combined with +.

trait Identified {
    fn id(self): int
}

trait Summary {
    fn summary(self): string
}

trait Record: Identified + Summary {
    fn line(self): string {
        return "#${self.id()} ${self.summary()}"
    }
}

struct Ticket { number: int, title: string }

impl Identified for Ticket {
    fn id(self): int { return self.number }
}

impl Summary for Ticket {
    fn summary(self): string { return self.title }
}

impl Record for Ticket {}

fn show[T](value: T): string
where T: Record {
    return value.line()
}

fn main(): void {
    println(show(Ticket { number: 7, title: "leak" }))
}

A T: Record bound also grants the parent methods, so show could call value.id() directly. Supertraits are obligations, not inheritance: they add no base fields and create no parent object at runtime.

The obligation is not yet checked at the impl site — writing impl Record for Ticket {} without the two parent implementations passes type checking and only breaks when line() is lowered. See Implementations.

Cycles are rejected:

trait Left: Right {}
trait Right: Left {}

That is ATOLL2103: trait inheritance cycle: Left -> Right.

Associated types

An associated type lets each implementation pick one type tied to itself, instead of forcing every caller to thread an extra parameter.

trait Source {
    type Item
    fn next_item(mut self): Option[Item]
    fn describe(self): string
}

struct Ticker { n: int }

impl Source for Ticker {
    type Item = int

    fn next_item(mut self): Option[int] {
        if self.n <= 0 { return None }
        self.n = self.n - 1
        return Some(self.n)
    }

    fn describe(self): string { return "ticker(${self.n})" }
}

struct Lines { text: string }

impl Source for Lines {
    type Item = string

    fn next_item(mut self): Option[string] {
        if self.text.is_empty() { return None }
        return Some(self.text)
    }

    fn describe(self): string { return "lines" }
}

fn main(): void {
    mut t := Ticker { n: 3 }
    println("${t.next_item()} ${t.describe()}")
    mut l := Lines { text: "a" }
    println("${l.next_item()} ${l.describe()}")
}

Inside the trait, refer to the associated type by its bare name (Item). Implementations bind it with type Item = … before the methods that use it are checked. Generic code names it through the constraint as T::Item; that projection is resolved from the selected implementation during specialization, never by a runtime lookup. See Projections.

An associated type can carry a bound (type Item: Show), which lets the trait’s own bodies call that trait’s methods, and it can carry a default (type Tally = int), which an implementation may then omit:

trait Show {
    fn show(self): string
}

trait Source {
    type Item: Show
    fn next_item(mut self): Option[Item]
}

trait Counter {
    type Tally = int
    fn tally(self): Tally
}

struct Word { text: string }

impl Show for Word {
    fn show(self): string { return self.text }
}

struct Words { one: Word }

impl Source for Words {
    type Item = Word
    fn next_item(mut self): Option[Word] { return Some(self.one) }
}

struct Hits { n: int }

// `Tally` is not bound here — the trait default supplies `int`.
impl Counter for Hits {
    fn tally(self): int { return self.n }
}

fn main(): void {
    mut w := Words { one: Word { text: "hi" } }
    match w.next_item() {
        Some(word) => println(word.show())
        None => println("none")
    }
    println("${Hits { n: 4 }.tally()}")
}

Reach for an associated type when one implementation has exactly one natural item type. Use a trait type parameter when a single type should be able to implement the trait several ways — Convert[Feet] and Convert[Yards] for the same source type.

Associated constants

A trait can declare a constant and an implementation can bind it.

trait Limits {
    const MAX: int
    fn clamp_low(self, v: int): int {
        if v < 0 { return 0 }
        return v
    }
}

struct Small {}

impl Limits for Small {
    const MAX: int = 10
}

The declaration and the binding are implemented, but the lookup spelling is not stabilized — reading the constant back does not compile today:

trait Limits {
    const MAX: int
}

struct Small {}

impl Limits for Small {
    const MAX: int = 10
}

fn f(): int {
    return Small.MAX
}

Until that settles, expose the value as a static method (fn max(): int) rather than an associated constant, and do not make associated-constant access part of a public API.

Sealing

@sealed restricts implementations to the trait’s declaring file.

@sealed
trait RuntimeHandle {
    fn raw(self): u32
}

struct TaskHandle { slot: u32 }

impl RuntimeHandle for TaskHandle {
    fn raw(self): u32 { return self.slot }
}

Seal a trait when the compiler or the runtime needs to enumerate every implementation, or when representation safety depends on controlling the set. The restriction follows the trait itself, and an implementation outside the declaring file is rejected during project resolution. Do not seal merely to reserve a method name.

Opaque use

impl Trait accepts or produces a value through the trait contract alone.

trait Shape {
    fn area(self): float
}

struct Square { side: float }
struct Circle { radius: float }

impl Shape for Square {
    fn area(self): float { return self.side * self.side }
}

impl Shape for Circle {
    fn area(self): float { return 3.14159 * self.radius * self.radius }
}

fn total_area(a: impl Shape, b: impl Shape): float {
    return a.area() + b.area()
}

fn main(): void {
    println("${total_area(Square { side: 1.0 }, Circle { radius: 1.0 })}")
    println("${total_area(Square { side: 2.0 }, Square { side: 3.0 })}")
}

This is still static dispatch. In parameter position each call passes one concrete type, and total_area is specialized per pair — the two impl Shape parameters are independent, so Square/Circle and Square/Square are two different instantiations.

In return position (fn unit(): impl Shape) the function owns one hidden concrete type that callers cannot name or equate with another function’s. That form type-checks but does not yet lower: using the result — calling area() on it, or passing it to total_area — fails at build time. Return the concrete type until that is finished. See Generics.

Dispatch is static

There is no dyn Trait, no vtable, and no downcast operator. Every trait call is resolved at check time and specialized per concrete receiver. The consequences worth planning around:

  • a []T where T: Shape is homogeneous — one element type, not a mixed bag;
  • a trait method cannot be stored and dispatched dynamically at runtime;
  • when alternatives must stay open at runtime, model them with an enum or an anonymous union and match on them.
trait Shape {
    fn area(self): float
}

struct Square { side: float }
struct Circle { radius: float }

impl Shape for Square {
    fn area(self): float { return self.side * self.side }
}

impl Shape for Circle {
    fn area(self): float { return 3.14159 * self.radius * self.radius }
}

// A runtime-heterogeneous collection needs an enum, not a trait object.
enum AnyShape {
    Sq(Square)
    Ci(Circle)
}

fn area_of(shape: AnyShape): float {
    match shape {
        Sq(s) => s.area()
        Ci(c) => c.area()
    }
}

fn main(): void {
    shapes := [Sq(Square { side: 2.0 }), Ci(Circle { radius: 1.0 })]
    mut total := 0.0
    for shape in shapes {
        total = total + area_of(shape)
    }
    println("${total}")
}

The prelude catalogue

These traits ship in the prelude and back the language’s own syntax. Their declarations live in crates/atoll-sema/src/stubs/traits.at.

Trait Required methods Backs
Equatable equals(self, Self): bool ==, !=
Hashable: Equatable hash_code(self): int Map keys, Set members
Comparable[T]: Equatable compare_to(self, T): int <, <=, >, >=, sorted, min, max
Display to_string(self): string "${x}", println
Debug debug_string(self): string debug formatting
Numeric: Comparable[Self] to_int, to_float, add, sub, mul, div +, -, *, /
Neg / Not / BitNot neg / not / bit_not -x, !x, ~x
BitAnd / BitOr / BitXor bit_and / bit_or / bit_xor &, |, ^
Shl / Shr shl(self, int) / shr(self, int) <<, >>
Index[K, V] get(self, K): V xs[k]
IndexMut[K, V] set(mut self, K, V): void xs[k] = v
Default default(): Self Type.default()
Clone clone(self): Self explicit copies
From[T] / Into[T] from(T): Self / into(self): T conversions
Iterator[T] next(mut self): Option[T] cursors, combinators
Iterable[T] type Iter, iterator(self): Iter for x in xs
Bytes / Buffer byte_len, byte_ptr / append, clear, … byte views and builders
Drop drop(self): () custom destructors
Selectable[T] raw_handle(self): usize, await(self): T select / race arms

There is no Add trait — arithmetic goes through Numeric, which requires all four operations plus Comparable[Self]. Operator syntax never bypasses the contract: primitives lower directly, and every non-primitive operand resolves through the applicable implementation. The worked examples for each operator family are in Implementations.

Display

Implementing Display is what makes a type usable in string interpolation.

struct Duration { millis: int }

impl Display for Duration {
    fn to_string(self): string {
        if self.millis < 1000 { return "${self.millis}ms" }
        return "${self.millis / 1000}s"
    }
}

fn main(): void {
    // `${...}` requires Display on the operand type.
    println("elapsed ${Duration { millis: 1500 }}")
}

Without it, interpolation is rejected with ATOLL3100: type … does not implement Display.

Iterator and Iterable

Iterator[T] is a one-shot cursor: supply next, and the type can drive a for loop. Iterable[T] produces a fresh cursor on demand — supply an associated Iter type and an iterator() method — which is what lets the same collection be walked more than once.

struct Countdown { remaining: int }

impl Iterator[int] for Countdown {
    fn next(mut self): Option[int] {
        if self.remaining <= 0 { return None }
        self.remaining = self.remaining - 1
        return Some(self.remaining)
    }
}

struct Fuse { length: int }

impl Iterable[int] for Fuse {
    type Iter = Countdown
    fn iterator(self): Countdown {
        return Countdown { remaining: self.length }
    }
}

fn main(): void {
    // An Iterator drives `for` directly, and is consumed by it.
    mut cursor := Countdown { remaining: 3 }
    for tick in cursor {
        println("tick ${tick}")
    }

    // An Iterable makes `for` work too. Bind the value first: a struct literal
    // in the iterable position would swallow the loop body.
    fuse := Fuse { length: 3 }
    for tick in fuse {
        println("fuse ${tick}")
    }

    // Explicit cursor use, when you want partial consumption.
    mut it := fuse.iterator()
    println("${it.next()} ${it.next()}")
}

The trait declarations also carry default combinator bodies — count, any, all, find, for_each on Iterator[T], and count_items and friends on Iterable[T]. They are not yet usable on a user implementation: the implicit Self: Iterator[T] bound their bodies rely on is not discharged for a concrete impl, so the call type-checks and then fails to lower.

struct Countdown { remaining: int }

impl Iterator[int] for Countdown {
    fn next(mut self): Option[int] {
        if self.remaining <= 0 { return None }
        self.remaining = self.remaining - 1
        return Some(self.remaining)
    }
}

fn main(): void {
    mut cursor := Countdown { remaining: 3 }
    println("${cursor.count()}")
}

That reports ATOLL3100: type Countdown does not implement trait Iterator (required by count). Write the loop yourself, or expose the combinator as an inherent method on your type:

struct Countdown { remaining: int }

impl Iterator[int] for Countdown {
    fn next(mut self): Option[int] {
        if self.remaining <= 0 { return None }
        self.remaining = self.remaining - 1
        return Some(self.remaining)
    }
}

impl Countdown {
    fn total(mut self): int {
        mut n := 0
        for v in self {
            n = n + v
        }
        return n
    }
}

fn main(): void {
    mut cursor := Countdown { remaining: 4 }
    println("${cursor.total()}")
}

The prelude collections ([]T, Map, Set, Stream) are unaffected — their combinators have real lowerings.

Evolving a public trait

  • adding a required method breaks every implementation;
  • adding a method with a valid default keeps implementations complete, but can collide with an inherent method of the same name;
  • changing a signature, error type, or effect row is a breaking change;
  • adding a supertrait adds an obligation everywhere;
  • changing an associated type or constant changes implementations and generic users;
  • sealing an open trait removes downstream extension rights.

Prefer small, capability-focused traits. Write defaults using only the trait’s own requirements and declared bounds — never an assumption about how a particular implementation is represented.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close