Skip to content

Implementations

Attach behavior to a type — inherent methods, trait implementations, generic and blanket impls, derivation, and operator overloading.

Updated View as Markdown

An implementation attaches methods to a type. There are two kinds — inherent (methods that belong to the type alone) and trait (methods that satisfy a contract) — and three places to write an inherent one.

struct Article {
    title: string
    body: string
}

// 1. Inside the declaration.
struct Draft {
    title: string

    fn is_blank(self): bool => self.title.is_empty()
}

// 2. An inherent `impl` block.
impl Article {
    fn word_count(self): int => self.body.split(" ").len()
}

// 3. A receiver-prefix function.
fn Article.headline(self): string => "${self.title} (${self.word_count()} words)"

fn main(): void {
    a := Article { title: "ports", body: "one two three" }
    println(a.headline())
    println("${Draft { title: "" }.is_blank()}")
}

All three forms produce the same static member call. Pick by locality: put construction and validation next to the declaration, and use an impl block or a receiver-prefix function when the behavior belongs to the module that needs it rather than to the module that declared the type.

Trait implementations

impl Trait for Type supplies a contract.

trait Summary {
    fn summary(self): string
}

struct Article { title: string }
struct Comment { author: string, text: string }

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

impl Summary for Comment {
    fn summary(self): string { return "${self.author}: ${self.text}" }
}

fn digest[T](items: []T): string
where T: Summary {
    mut out := ""
    for item in items {
        if out.len() > 0 { out = out + " | " }
        out = out + item.summary()
    }
    return out
}

fn main(): void {
    println(digest([Article { title: "Ports" }, Article { title: "Cranes" }]))
    println(digest([Comment { author: "ada", text: "nice" }]))
}

Completeness

Every required method must be present. A method carrying a trait default may be omitted.

trait Named {
    fn name(self): string

    fn is_anonymous(self): bool {
        return self.name().is_empty()
    }
}

struct Account { handle: string }

// `is_anonymous` is inherited from the default.
impl Named for Account {
    fn name(self): string { return self.handle }
}

fn main(): void {
    println("${Account { handle: "" }.is_anonymous()}")
}

Omitting a required method is diagnosed:

trait Summary {
    fn summary(self): string
    fn headline(self): string
}

struct Article { title: string }

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

That is ATOLL2105: impl is missing required trait methods: headline.

So is adding one the trait does not declare — extra behavior belongs in an inherent block:

trait Summary {
    fn summary(self): string
}

struct Article { title: string }

impl Summary for Article {
    fn summary(self): string { return self.title }
    fn word_count(self): int { return 1 }
}

ATOLL2105: method word_count is not declared on trait Summary. Split it:

trait Summary {
    fn summary(self): string
}

struct Article { title: string, body: string }

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

impl Article {
    fn word_count(self): int { return self.body.split(" ").len() }
}

fn main(): void {
    a := Article { title: "t", body: "a b c" }
    println("${a.summary()} ${a.word_count()}")
}

Signature agreement

An implementation must match the requirement’s full contract: receiver mutability, parameter types, success type, error type, and effect row.

Be aware that the current checker does not verify parameter and return types at the impl site — a mismatched signature is accepted there and only fails later where the method is called with the trait’s declared types. Treat the requirement as authoritative and copy the signature verbatim; do not rely on the impl block to catch a typo.

Generic implementations

Declare the implementation’s own parameters after impl.

struct Box[T] { value: T }

impl[T] Box[T] {
    fn of(value: T): Box[T] => Box { value: value }
    fn get(self): T => self.value
    fn replace(self, next: T): Box[T] => Box { value: next }
}

fn main(): void {
    b := Box.of(3)
    println("${b.replace(9).get()}")
}

A conditional trait implementation applies only when its bounds hold:

struct Box[T] { value: T }

impl[T] Equatable for Box[T]
where T: Equatable {
    fn equals(self, other: Box[T]): bool {
        return self.value == other.value
    }
}

impl[T] Display for Box[T]
where T: Display {
    fn to_string(self): string {
        return "Box(${self.value})"
    }
}

fn main(): void {
    println("${Box { value: 3 } == Box { value: 3 }} ${Box { value: "x" }}")
}

Box[Token] satisfies Equatable only when Token does.

A blanket implementation targets a type variable, making the trait hold for every type meeting the predicate:

trait Printable: Display + Debug {}

impl[T] Printable for T
where T: Display + Debug {}

struct Tag { name: string }

impl Display for Tag {
    fn to_string(self): string { return self.name }
}

impl Debug for Tag {
    fn debug_string(self): string { return "Tag(${self.name})" }
}

fn emit[T](value: T): string
where T: Printable {
    return value.to_string()
}

fn main(): void {
    println(emit(Tag { name: "release" }))
}

The body of a generic implementation is checked against its declared parameters and bounds before any specialization, so an error there points at the implementation, not at some downstream call.

Coherence

Exactly one implementation may be selected for a trait/type pair. Duplicates are rejected:

trait Summary {
    fn summary(self): string
}

struct Article { title: string }

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

impl Summary for Article {
    fn summary(self): string { return "other" }
}

ATOLL2102: duplicate trait implementation. The compiler also diagnoses overlapping unconditional blanket implementations, generic implementations whose constraints do not prove disjointness, unknown traits, missing required methods, and methods not declared by the trait.

Atoll uses whole-project coherence rather than an orphan rule: any file may implement an in-scope trait for an in-scope type, including declarations it does not own. That is convenient for integration modules, but it means two independent adapters for the same pair conflict at link time. Put such an implementation in one clearly owned integration module and treat it as project-wide policy — there is no file-local or import-local method table.

The overlap checker compares concrete targets, blanket target patterns, and their predicates. Two unconditional blankets overlap; two blankets with the same effective predicate overlap; concrete implementations for different types do not. Do not lean on subtle predicate disjointness in a public design when one explicit wrapper type would make ownership obvious.

Supertrait obligations

A trait declared as trait Record: Identified says its implementors are also expected to satisfy Identified, because defaults and bounds may call the parent methods.

trait Identified {
    fn id(self): int
}

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

struct Ticket { number: int }

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

impl Record for Ticket {}

fn main(): void {
    println(Ticket { number: 7 }.line())
}

Supply the parent implementation alongside the child one, as above. A generic bound T: Record grants the parent methods too, so callers do not have to repeat T: Identified.

The obligation is a design contract, not yet a checked one. impl Record for Ticket {} with no impl Identified for Ticket passes type checking; nothing points at the missing parent. It fails only when something actually reaches for id() — and then with an internal lowering error rather than a useful message:

trait Identified {
    fn id(self): int
}

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

struct Ticket { number: int }

// No `impl Identified for Ticket` — accepted here, fatal at build.
impl Record for Ticket {}

fn main(): void {
    println(Ticket { number: 7 }.line())
}

Write the parent implementation.

Associated types

Bind an associated type with type Name = Type inside the implementation.

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

struct Countdown { remaining: int }

impl Source for Countdown {
    type Item = int

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

fn main(): void {
    mut c := Countdown { remaining: 2 }
    println("${c.next_item()} ${c.next_item()} ${c.next_item()}")
}

The binding participates in method type checking and in generic dispatch: a caller with T: Source sees T::Item resolved to int once T = Countdown is selected.

Sealed traits

A trait marked @sealed may be implemented only in its declaring file. This is a semantic restriction enforced during project resolution, not a lint.

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

struct TaskHandle { slot: u32 }

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

fn main(): void {
    println("${TaskHandle { slot: 3 }.raw()}")
}

Derivation

Field-wise equality and hashing are supplied automatically. A struct or tuple whose fields all support the operation is comparable with == and usable as a Map key or Set member with no annotation at all:

struct Key {
    namespace: string
    id: int
}

fn main(): void {
    a := Key { namespace: "cache", id: 1 }
    b := Key { namespace: "cache", id: 1 }
    println("${a == b}")

    // Structural hashing is what admits the type as a Map key / Set member.
    mut seen: Set[Key] = Set.new()
    seen.add(a)
    seen.add(b)
    println("${seen.size()}")
}

@derive(Equatable, Hashable) parses and is recorded on the declaration, but it is not yet wired to anything: it synthesizes nothing the compiler would not already have supplied, and an unsupported name in the list is accepted in silence. Treat it as documentation of intent, not as a mechanism.

Write the implementation by hand when the field-wise rule is wrong — for instance when equality should ignore a cache field. An explicit impl Equatable for Key replaces the structural one:

struct Key {
    namespace: string
    id: int
    hits: int
}

impl Equatable for Key {
    // `hits` is a counter, not identity.
    fn equals(self, other: Key): bool {
        return self.namespace == other.namespace && self.id == other.id
    }
}

fn main(): void {
    a := Key { namespace: "cache", id: 1, hits: 0 }
    b := Key { namespace: "cache", id: 1, hits: 97 }
    println("${a == b}")
}

Operator overloading

Operators on non-primitive operands resolve through prelude traits. Primitives keep their direct lowering; there is no way to change what + means on int. There is also no Add trait — arithmetic is the four-method Numeric contract.

Arithmetic: Numeric

Numeric requires add, sub, mul, div, plus to_int / to_float, and inherits Comparable[Self].

struct Vec2 { x: float, y: float }

impl Equatable for Vec2 {
    fn equals(self, other: Vec2): bool => self.x == other.x && self.y == other.y
}

impl Comparable[Vec2] for Vec2 {
    fn compare_to(self, other: Vec2): int {
        left := self.x * self.x + self.y * self.y
        right := other.x * other.x + other.y * other.y
        if left < right { return -1 }
        if left > right { return 1 }
        return 0
    }
}

impl Numeric for Vec2 {
    fn to_int(self): int => (self.x + self.y).to_int()
    fn to_float(self): float => self.x + self.y
    fn add(self, o: Vec2): Vec2 => Vec2 { x: self.x + o.x, y: self.y + o.y }
    fn sub(self, o: Vec2): Vec2 => Vec2 { x: self.x - o.x, y: self.y - o.y }
    fn mul(self, o: Vec2): Vec2 => Vec2 { x: self.x * o.x, y: self.y * o.y }
    fn div(self, o: Vec2): Vec2 => Vec2 { x: self.x / o.x, y: self.y / o.y }
}

fn main(): void {
    a := Vec2 { x: 1.0, y: 2.0 }
    b := Vec2 { x: 3.0, y: 4.0 }
    sum := a + b
    diff := b - a
    println("${sum.x},${sum.y} ${diff.x},${diff.y} ${(a < b)}")
}

Because Numeric also implies Comparable[Self], a Numeric type works with sorted, min, max, and sum on []T.

Comparison: Equatable and Comparable[T]

Equatable::equals backs == and !=. Comparable[T]::compare_to backs <, <=, >, >= and the ordering-based list methods.

struct Version { major: int, minor: int }

impl Equatable for Version {
    fn equals(self, other: Version): bool {
        return self.major == other.major && self.minor == other.minor
    }
}

impl Comparable[Version] for Version {
    fn compare_to(self, other: Version): int {
        if self.major != other.major { return self.major - other.major }
        return self.minor - other.minor
    }
}

impl Display for Version {
    fn to_string(self): string => "${self.major}.${self.minor}"
}

fn main(): void {
    releases := [
        Version { major: 1, minor: 4 },
        Version { major: 0, minor: 9 },
        Version { major: 1, minor: 0 },
    ]
    for v in releases.sorted() {
        println("${v}")
    }
    println("${releases.max()}")
}

Keep compare_to consistent with equals: compare_to returning 0 and equals returning false for the same pair breaks sorting and lookup.

Unary: Neg, Not, BitNot

-x calls neg, !x calls not, ~x calls bit_not.

struct Signal { level: int, muted: bool }

impl Neg for Signal {
    fn neg(self): Signal => Signal { level: -self.level, muted: self.muted }
}

impl Not for Signal {
    fn not(self): Signal => Signal { level: self.level, muted: !self.muted }
}

impl BitNot for Signal {
    fn bit_not(self): Signal => Signal { level: ~self.level, muted: self.muted }
}

fn main(): void {
    s := Signal { level: 5, muted: false }
    println("${(-s).level} ${(!s).muted} ${(~s).level}")
}

Bitwise and shifts

&, |, ^, <<, >> map to bit_and, bit_or, bit_xor, shl, shr. The shift traits take an int on the right, not Self.

struct Flags { bits: int }

impl BitAnd for Flags {
    fn bit_and(self, other: Flags): Flags => Flags { bits: self.bits & other.bits }
}

impl BitOr for Flags {
    fn bit_or(self, other: Flags): Flags => Flags { bits: self.bits | other.bits }
}

impl BitXor for Flags {
    fn bit_xor(self, other: Flags): Flags => Flags { bits: self.bits ^ other.bits }
}

impl Shl for Flags {
    fn shl(self, n: int): Flags => Flags { bits: self.bits << n }
}

impl Shr for Flags {
    fn shr(self, n: int): Flags => Flags { bits: self.bits >> n }
}

fn main(): void {
    read := Flags { bits: 0b0001 }
    write := Flags { bits: 0b0010 }
    both := read | write
    println("${both.bits} ${(both & read).bits} ${(both ^ read).bits}")
    println("${(read << 3).bits} ${(write >> 1).bits}")
}

Indexing: Index[K, V] and IndexMut[K, V]

xs[k] calls Index::get; xs[k] = v calls IndexMut::set. Unlike the built-in List index — which yields an Option — a user Index returns exactly the V you declare.

struct Row {
    cells: []int
}

impl Index[int, int] for Row {
    fn get(self, key: int): int {
        return self.cells.get(key.to_u32()) ?? 0
    }
}

impl IndexMut[int, int] for Row {
    fn set(mut self, key: int, value: int): void {
        self.cells.set(key.to_u32(), value)
    }
}

fn main(): void {
    mut r := Row { cells: [0, 0, 0, 0] }
    r[2] = 7
    // Out of range returns the declared fallback, not an Option.
    println("${r[2]} ${r[99]}")
}

K need not be an integer — Index[string, int] gives a struct dictionary-style access.

Text: Display

Display::to_string is what "${x}", println, and logging use.

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"
    }
}

impl Debug for Duration {
    fn debug_string(self): string => "Duration(millis: ${self.millis})"
}

fn main(): void {
    d := Duration { millis: 1500 }
    println("took ${d}")
    println(d.debug_string())
}

Conversion: From[T] and Into[T]

struct Celsius { degrees: float }
struct Fahrenheit { degrees: float }

impl From[Celsius] for Fahrenheit {
    fn from(value: Celsius): Fahrenheit {
        return Fahrenheit { degrees: value.degrees * 1.8 + 32.0 }
    }
}

impl Into[Fahrenheit] for Celsius {
    fn into(self): Fahrenheit {
        return Fahrenheit { degrees: self.degrees * 1.8 + 32.0 }
    }
}

fn main(): void {
    boiling := Fahrenheit.from(Celsius { degrees: 100.0 })
    freezing: Fahrenheit = Celsius { degrees: 0.0 }.into()
    println("${boiling.degrees} ${freezing.degrees}")
}

From is the static direction (Target.from(source)); Into is the method direction (source.into()), and the destination annotation is what picks the target type.

Defaults and copies: Default, Clone

struct Config {
    retries: int
    verbose: bool
}

impl Default for Config {
    fn default(): Config => Config { retries: 3, verbose: false }
}

impl Clone for Config {
    fn clone(self): Config => Config { retries: self.retries, verbose: self.verbose }
}

fn with_retries(base: Config, n: int): Config {
    mut next := base.clone()
    next.retries = n
    return next
}

fn main(): void {
    base := Config.default()
    tuned := with_retries(base, 10)
    println("${base.retries} ${tuned.retries}")
}

Method selection

A member call resolves from the receiver’s static type plus the bounds in scope. There is no runtime search through an open method table: if selection is ambiguous or no implementation exists, compilation fails.

Methods from a generic bound are available only through that bound. An unbounded parameter does not acquire a method just because some reachable instantiation happens to implement it:

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 {
    return value.summary()
}

When an inherent method and a trait method share a name, design so the receiver’s static type and the required trait make the choice obvious. Atoll does not defer ambiguity to runtime.

Choosing a form

Need Form
Behavior intrinsic to one nominal type impl Type or a method in the declaration
Behavior added by the consuming module fn Type.method(self)
Satisfy a reusable capability impl Trait for Type
Same capability across a constrained family impl[T] Trait for Box[T] where …
Capability for everything meeting a predicate Blanket impl[T] Trait for T where …
Field-wise equality and hashing Nothing — it is structural already
Adapter joining two external declarations One owned integration module
Compiler-controlled closed set @sealed trait

Keep construction and validation in inherent methods; use traits for behavior whose contract makes sense across unrelated types. A blanket implementation is project-wide policy, so make its bounds as narrow as you can.

A composed example

Inherent methods, several trait implementations including operator overloading, and a fallible method, on one small model.

error CartError { Empty }

struct Money { cents: int }

impl Equatable for Money {
    fn equals(self, other: Money): bool => self.cents == other.cents
}

impl Comparable[Money] for Money {
    fn compare_to(self, other: Money): int => self.cents - other.cents
}

impl Numeric for Money {
    fn to_int(self): int => self.cents
    fn to_float(self): float => self.cents.to_float() / 100.0
    fn add(self, o: Money): Money => Money { cents: self.cents + o.cents }
    fn sub(self, o: Money): Money => Money { cents: self.cents - o.cents }
    fn mul(self, o: Money): Money => Money { cents: self.cents * o.cents / 100 }
    fn div(self, o: Money): Money => Money { cents: self.cents * 100 / o.cents }
}

impl Display for Money {
    fn to_string(self): string => "USD ${self.cents / 100}.${self.cents % 100}"
}

struct Line {
    label: string
    price: Money
    qty: int
}

impl Line {
    fn subtotal(self): Money => Money { cents: self.price.cents * self.qty }
}

struct Cart {
    lines: []Line
}

impl Cart {
    fn empty(): Cart => Cart { lines: [] }

    fn add_line(mut self, line: Line): void {
        self.lines.add(line)
    }

    fn total(self): Money ! CartError {
        if self.lines.is_empty() { error Empty }
        mut sum := Money { cents: 0 }
        for line in self.lines {
            sum = sum + line.subtotal()
        }
        return sum
    }
}

impl Display for Cart {
    fn to_string(self): string {
        mut out := ""
        for line in self.lines {
            if out.len() > 0 { out = out + "; " }
            out = out + "${line.qty}x ${line.label} @ ${line.price}"
        }
        return out
    }
}

fn main(): void {
    mut cart := Cart.empty()
    cart.add_line(Line { label: "rope", price: Money { cents: 1250 }, qty: 2 })
    cart.add_line(Line { label: "hook", price: Money { cents: 399 }, qty: 1 })
    println("${cart}")
    println("${cart.total().unwrap_or(Money { cents: 0 })}")
}

Money gains + from Numeric, ordering from Comparable[Money], and interpolation from Display; Cart keeps its construction and its fallible aggregate in an inherent block, and borrows Display for rendering.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close