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 impl blocks

A trait declares a set of methods; a type joins the trait by writing an impl block. Satisfaction is nominal — the impl block is the only admission. A type whose members all happen to match a trait’s shapes is still not an instance of it until someone writes the block. The full dispatch story is in the reference on traits and traits and dispatch.

Declaring a trait

Traits declare method signatures — no bodies, no default implementations, no fields:

trait Shape {
    fn area(self) -> f64;
    fn name(self) -> str;
}

Methods spell their receiver self explicitly, like every rut method. Anything that looks like a property is a method: declare fn count(self) -> i32;, not a field.

Implementing a trait

impl Trait for Type { .. } registers the pair. Every method must match the trait’s signature exactly; extra methods don’t belong here (put those in an inherent block). Trait impl methods carry no pub — they are as visible as the trait.

use ink::{ Logger };

trait Shape {
    fn area(self) -> f64;
    fn name(self) -> str;
}

struct Circle { r: f64 }

impl Shape for Circle {
    fn area(self) -> f64 { return 3.14159 * self.r * self.r; }
    fn name(self) -> str { return "circle"; }
}

pub fn main() {
    let log = Logger.new("traits");
    let c = Circle { r: 1.0 };
    log.info(f"{c.name()}={c.area()}");
}
circle=3.14159

Rules worth knowing:

  • One impl per (trait, type) pair, program-wide. A duplicate is a link error.
  • Placement: an impl may live in a module of the trait’s package or the type’s package — at least one side must be yours. You cannot implement two foreign types to each other.
  • An empty impl block is legal and acts as a marker: impl Serializable for Point {} says “this type is in” when the trait has no required methods.
  • Primitives can implement traits too (in the trait’s own package or module) — the standard library uses this to give integer widths a common internal interface.

Inherent impls

impl Type { .. } — no trait — is where a type’s own methods live: class methods like new, instance methods, helpers. It compiles only in the module that declares the type.

use ink::{ Logger };

struct Circle { r: f64 }

impl Circle {
    fn new(r: f64) -> Self {
        return Self { r: r };
    }
}

pub fn main() {
    let log = Logger.new("traits");
    let c = Circle.new(1.0);
    log.info(f"r={c.r}");
}
r=1

Using trait values

Widening is implicit at any position that expects the trait: an annotated binding, a field, an argument, a return.

use ink::{ Logger };

trait Shape {
    fn area(self) -> f64;
    fn name(self) -> str;
}

struct Circle { r: f64 }

impl Shape for Circle {
    fn area(self) -> f64 { return 3.14159 * self.r * self.r; }
    fn name(self) -> str { return "circle"; }
}

pub fn main() {
    let log = Logger.new("traits");
    let c: Shape = Circle { r: 1.0 };   // Circle widens to Shape
    log.info(f"{c.name()}={c.area()}");
}
circle=3.14159

Trait-typed parameters accept any implementor — write the function once, call it with each concrete type:

use ink::{ Logger };

trait Shape {
    fn area(self) -> f64;
    fn name(self) -> str;
}

struct Circle { r: f64 }
struct Square { side: f64 }

impl Shape for Circle {
    fn area(self) -> f64 { return 3.14159 * self.r * self.r; }
    fn name(self) -> str { return "circle"; }
}

impl Shape for Square {
    fn area(self) -> f64 { return self.side * self.side; }
    fn name(self) -> str { return "square"; }
}

fn describe(s: Shape) -> str {
    return f"{s.name()}={s.area()}";
}

pub fn main() {
    let log = Logger.new("traits");
    log.info(describe(Circle { r: 1.0 }));
    log.info(describe(Square { side: 3.0 }));
}
circle=3.14159
square=9

Heterogeneous containers hold mixed implementors behind the trait name. Inside a single-typed context calls bind directly; iterating a collection of Shape dispatches through the value’s vtable — the compiler picks, and mis-guessing costs one hop, never wrong behavior:

use pouch::{ Vec };
use ink::{ Logger };

trait Shape {
    fn area(self) -> f64;
    fn name(self) -> str;
}

struct Circle { r: f64 }
struct Square { side: f64 }

impl Shape for Circle {
    fn area(self) -> f64 { return 3.14159 * self.r * self.r; }
    fn name(self) -> str { return "circle"; }
}

impl Shape for Square {
    fn area(self) -> f64 { return self.side * self.side; }
    fn name(self) -> str { return "square"; }
}

pub fn main() {
    let log = Logger.new("traits");
    let mut shapes: Vec<Shape> = Vec.new();
    shapes.push(Circle { r: 1.0 });
    shapes.push(Square { side: 3.0 });
    let mut total: f64 = 0.0;
    for (let s of shapes) {
        total += s.area();       // dispatches per element
    }
    log.info(f"total={total} n={shapes.len()}");
}
total=12.14159 n=2

Type tests: is

expr is Type answers with a bool and never traps:

  • Concrete RHS — p is Circle: an exact-type test.
  • Trait RHS — p is Shape: a capability probe (“does this value’s type have an impl of Shape?”).
use ink::{ Logger };

trait Shape {
    fn area(self) -> f64;
    fn name(self) -> str;
}

struct Circle { r: f64 }
struct Square { side: f64 }

impl Shape for Circle {
    fn area(self) -> f64 { return 3.14159 * self.r * self.r; }
    fn name(self) -> str { return "circle"; }
}

impl Shape for Square {
    fn area(self) -> f64 { return self.side * self.side; }
    fn name(self) -> str { return "square"; }
}

pub fn main() {
    let log = Logger.new("traits");
    let sq = Square { side: 2.0 };
    log.info(f"shape: {sq is Shape} circle: {sq is Circle}");
}
shape: true circle: false

is answers the question; it changes nothing — there is no narrowing and no downcast through a trait. A trait-typed value is used through its trait’s methods; if you need the erased-storage version — a value whose type is forgotten until recovered — that is opaque, covered in errors and optionality and the reference on opaque.

Bounds connect traits to generics

fn name<T requires Labeled>(x: T) admits exactly the instantiations whose concrete type has an impl of Labeled — see functions, closures, and generics. The bound is what makes widening legal inside the body: let w: Labeled = x;.

Making your type iterable

A type becomes a for..of target by implementing the builtin Iterator<E> contract with its single resumption member:

use pouch::{ Vec };
use ink::{ Logger };

struct CountUp { n: i32 }

impl Iterator<i32> for CountUp {
    fn __iterate(self, emit: fn(i32) -> bool) {
        for (let i = 1; i <= self.n; i += 1) {
            if (!emit(i)) { return; }   // false = stop
        }
    }
}

pub fn main() {
    let log = Logger.new("iter");
    let ups = CountUp { n: 4 };
    let got: Vec<i32> = Vec.new();   // shared: survives the loop's captures
    for (let v of ups) {
        got.push(v);
    }
    log.info(f"n={got.len()} first={got[0]} last={got[got.len()-1]}");
}
n=4 first=1 last=4

for (let v of it) desugars to it.__iterate(emit) with a synthetic closure. One consequence to know: the loop body’s captures are taken at the desugar, so reassigning an enclosing scalar inside the loop mutates a copy, not the original. Accumulate through something shared instead — vec.push(v) — or have the loop body act on values it can see directly.

The builtin sequences ([T], Vec<T>, str, bytes) iterate without the trait — their loops are fused, never a per-element call.

Put it together

use pouch::{ Vec };
use ink::{ Logger };

trait Shape {
    fn area(self) -> f64;
    fn name(self) -> str;
}

struct Circle { r: f64 }
struct Square { side: f64 }

impl Shape for Circle {
    fn area(self) -> f64 { return 3.14159 * self.r * self.r; }
    fn name(self) -> str { return "circle"; }
}

impl Shape for Square {
    fn area(self) -> f64 { return self.side * self.side; }
    fn name(self) -> str { return "square"; }
}

fn describe(s: Shape) -> str {
    return f"{s.name()}={s.area()}";
}

pub fn main() {
    let log = Logger.new("traits");

    let c: Shape = Circle { r: 1.0 };
    log.info(describe(c));

    let sq = Square { side: 2.0 };
    log.info(f"is shape: {sq is Shape}");

    let mut shapes: Vec<Shape> = Vec.new();
    shapes.push(Circle { r: 1.0 });
    shapes.push(Square { side: 3.0 });
    let mut total: f64 = 0.0;
    for (let s of shapes) {
        total += s.area();
    }
    log.info(f"total={total} n={shapes.len()}");
}
circle=3.14159
is shape: true
total=12.14159 n=2

Next: errors and optionality.