Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Traits and Methods

A trait names a capability. A type implements the trait by providing the required methods and associated types, and generic callers can require that capability with a bound.

Declaring and Implementing a Trait

This trait says that an implementor can calculate an area through a shared receiver. Two unrelated structs provide different implementations.

struct Circle
    < radius: I64

struct Rectangle
    < width: I64
    < height: I64

trait Shape
    @area: I64

impl Shape for Circle
    @area = -> @radius * @radius * 3

impl Shape for Rectangle
    @area = -> @width * @height

main = !->
    circle = Circle
        radius: 5
    rectangle = Rectangle
        width: 4
        height: 6
    circle.area!.println!
    rectangle.area!.println!

circle.area! selects the Circle implementation and returns 75; rectangle.area! selects the Rectangle implementation and returns 24. The output is 75 and 24.

The @ marker is part of the method contract. The trait signature omits an explicit self parameter because the receiver is implicit; the body can use @radius, @width, or self.

Default Methods

A trait may provide a method body. An implementation can override one method and inherit the others.

struct Dog
    < name: I64

struct Cat
    < name: I64

trait Animal
    @speak: I64
    @speak = -> 0
    @legs: I64
    @legs = -> 4

impl Animal for Dog
    @speak = -> 1

impl Animal for Cat
    @speak = -> 2

main = !->
    dog = Dog
        name: 10
    cat = Cat
        name: 20
    dog.speak!.println!
    cat.speak!.println!
    dog.legs!.println!
    cat.legs!.println!

Dog and Cat override speak! but inherit legs!. The output is 1, 2, 4, and 4.

Inherent Implementations

An inherent implementation adds methods directly to a type without defining a trait. Associated functions have no receiver and are called through the type path.

struct Point
    < x: I64
    < y: I64

impl Point
    new: I64 -> I64 -> Point
    new = x, y ->
        Point
            x: x
            y: y

    @sum: I64
    @sum = -> @x + @y

main = !->
    point = Point::new 3, 4
    point.sum!.println!

Point::new is an associated function and returns a Point; sum! is a shared method. The output is 7.

Receiver Ownership

The receiver marker also controls ownership.

struct Counter
    < value: I64

impl Counter
    @read: I64
    @read = -> @value

    ^@set: I64 -> ()
    ^@set = value !->
        self.value = value

    ~@take: I64
    ~@take = -> self.value

main = !->
    mut counter = Counter
        value: 1
    counter.read!.println!
    counter.set 9
    counter.read!.println!
    counter.take!.println!

@read shared-borrows, ^@set requires a mutable binding and writes in place, and ~@take consumes the receiver. The output is 1, 9, and 9; counter cannot be used after take!.

Associated Types

An associated type lets an implementation choose one output type for a trait.

trait Projector
    type Output
    @project: Self::Output -> Self::Output

struct Identity

impl Projector for Identity
    type Output = I64
    @project = value -> value

main = !->
    identity = Identity
    identity.project 13 .println!

The implementation defines Identity::Output = I64, so project accepts and returns I64. Omitting the required associated type is a compile-time error.

Generic Trait Bounds

A generic function can use a trait method or operator when its signature names the bound.

struct Box T
    < value: T

same_box: Box T -> Box T -> Bool where T: Eq
same_box = left, right -> left.value == right.value

main = !->
    first = Box
        value: 7
    second = Box
        value: 7
    same_box first, second .println!

The call specializes T to I64, and the Eq implementation for I64 supplies ==. The output is true. A bound is checked at the call site as well as in the generic body.

Focused Standard Traits

The standard library uses the same mechanism for Show and Clone. This type implements both: show! creates display text, while clone! creates independent owned data.

struct Label
    < text: String

impl Show for Label
    @show: String
    @show = -> @text.clone!

impl Clone for Label
    @clone: Label
    @clone = ->
        Label
            text: @text.clone!

main = !->
    original = Label
        text: String::from "Rock"
    copy = original.clone!
    original.println!
    copy.println!

The output is Rock twice. Cloning the String gives each Label its own allocation, while Show borrows the receiver. Other focused standard traits follow the same selection model: Arrays, Slices, and Tuples demonstrates Index and IndexMut, and Functions as Values demonstrates Fn, FnMut, and FnOnce with complete callbacks. Keep a trait small; combine bounds only when one operation genuinely needs several capabilities.

Coherence and Current Limits

Rock requires one unambiguous applicable implementation for a trait call. The following exhaustive program is intentionally rejected because Tag has two implementations of the same trait:

trait Named
    @name: &Str

struct Tag

impl Named for Tag
    @name = -> "first"

impl Named for Tag
    @name = -> "second"

main = !->
    tag = Tag
    tag.name!.println!

Rock does not choose first or second by declaration order; remove one implementation to make the call coherent. Missing associated type definitions are rejected for the same reason that missing methods are rejected: an implementation must satisfy its complete trait contract. Trait selection also preserves ownership, so it cannot turn a borrowed receiver into an owned value or make an owned value copyable.