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.