Skip to content
CryoCryo home
LanguageAbstraction

12Implement Blocks

implement adds methods to a type without modifying its original declaration. There are three forms.

12.1 Inherent Implementation

Adds methods to a type. For a struct, an inherent block is interchangeable with declaring the methods inline:

implement struct Point {
    distance_to(&this, other: Point) -> f64 {
        const dx: f64 = (this.x - other.x) as f64;
        const dy: f64 = (this.y - other.y) as f64;
        return (dx * dx + dy * dy).sqrt();
    }
}

Inherent blocks are the only way to add methods to enums:

implement enum Color {
    to_string(&this) -> string {
        match (this) {
            Color::Red   => { return "red";   }
            Color::Green => { return "green"; }
            Color::Blue  => { return "blue";  }
        }
    }
}

12.2 Trait Implementation

Provides the method bodies a trait requires for a specific type. Use this to make T participate in generic code that has a where T: SomeTrait bound.

implement trait Eq for i32 {
    equals(&this, other: &i32) -> boolean {
        return this == *other;
    }
}

12.3 Implement Blocks on Primitives

Implement blocks may target primitive types, which is how the standard library hangs methods off boolean, the integer types, and string:

implement boolean {
    to_i32(&this) -> i32 {
        if (this) { return 1; }
        return 0;
    }

    static default() -> boolean {
        return false;
    }
}

After the block is loaded, my_bool.to_i32() resolves like any other method call. Resolution is at compile time; no dynamic dispatch.

A primitive owns methods and nothing else: no type declares constants, so i32::MAX names no value and is refused (E0201), through an alias of the primitive too. An integer type's bounds are its static methods, i32::max_value() and i32::min_value().

12.4 Which Method a Call Names

A method call recv.m(...) names the one method m its arguments bind to, and no kind of method outranks another. Two methods of one name that both fit the arguments make the call ambiguous, and it is an error; in particular, a non-generic method does not win over a generic one by being non-generic:

type struct Box2 { v: i32; }
implement struct Box2 {
    put(&this, x: i32) -> i32 { return x; }
    put<T>(&this, x: T) -> i32 { return 0; }
}

const b: Box2 = Box2 { v: 0 };
const n: i32 = b.put(1);    // error: both `put`s apply

A static method is chosen the same way, its generic overloads by the type arguments its arguments (or its turbofish) bind. With conv<U>(u: U) and conv<U>(u: U, w: U) on Plain, Plain::conv(7, 9) calls the second. A static call two overloads accept is E0154, whether both are generic or one is not, and a call none accepts is E0214.

A receiver with an inherent method m and a trait impl that also provides an m the arguments bind to makes the call ambiguous (E0156), whichever order the implement blocks were written in and whether the receiver or either method is generic. The path forms are the same call and the same tie: T::m(recv, ...), T::m() for a static method, and T::m as a value. The caller writes the impl-qualified path (Tr for T)::m(&recv, ...) below, or renames one of them. A name two traits provide is an error as well (E0156), whether the call is written recv.m(...), T::m(recv, ...), T::m() for a static method, or T::m as a value, and never a pick by declaration or import order. The error names each candidate where it is declared and the spelling that chooses. A call made through a bound (x.m() where x: &T and where T: Tr, or the path T::m(...) and the value T::m under the same bound) names Tr::m, since T has no methods but its bounds' where the call is written — the concrete type it is later instantiated with does not change that, even when it has an inherent m or implements a second trait providing m. A parameter bounded by two traits that both provide m directs nothing, and the instantiation reports the tie. A member none of the parameter's bounds declares is refused at the member: x.m() where it is written (E0358), and the path T::m(...) and the value T::m (E0358), or (Tr for T)::m where Tr declares no m (E0233), once the body is instantiated - a generic body nothing instantiates is not refused for such a path. On a concrete receiver there is no such direction, so an inherent method and a trait's method of one name that both fit are a tie, and the call is refused.

type trait Beta { go(&this, u: i32) -> i64; }
type struct IgTn { v: i32; }
implement trait Beta for IgTn { go(&this, u: i32) -> i64 { return 2; } }
implement struct IgTn { go<U>(&this, u: U) -> i64 { return 1; } }

function via<T>(x: &T) -> i64 where T: Beta { return x.go(0); }

const ig: IgTn = IgTn { v: 5 };
const a: i64 = ig.go(7);                     // error[E0156]: an inherent `go` and `Beta::go` both apply
const d: i64 = IgTn::go(&ig, 7);             // error[E0156]: the same tie through the path
const b: i64 = (Beta for IgTn)::go(&ig, 7);  // 2
const c: i64 = via::<IgTn>(&ig);             // 2: the bound names `Beta::go`

A method as a value. A method named by path without a call is a function value. A static method's value has the method's own signature; a method with a receiver takes the receiver as its first parameter: for step(&this, u: i32) -> i64, Pt::step is a (&Pt, i32) -> i64, called f(&p, 7); mut &this gives mut &Pt and this gives Pt. The value names one method, so an inherent m beside a trait's m is the same tie (E0156) and (Tr for T)::m chooses. A method generic in its own parameters, or a method of a generic owner, is taken as a value the same way, and its type arguments are inferred from the use, as a generic function's are (section 2.5): const g: (&Wrap<i32>) -> i32 = Wrap::get;. A generic owner's method with parameters of its own binds both sets: the owner's from the arguments written on the owner or the expected type, the method's from its turbofish or the expected type, so Wrap::<i32>::conv::<u8> and const k: (&Wrap<i32>, u16) -> i32 = Wrap::keep; are each one instance. A parameter nothing binds is E0307. An overloaded method is chosen as an overloaded function is: the overload whose value fits the turbofish and the expected function type, so with static methods conv<U>(u: U) and conv<U>(u: U, w: U) on Plain, const f: (u8, u8) -> u8 = Plain::conv::<u8>; takes the second. When no expected type and no turbofish choose, or more than one overload fits, the value is refused as ambiguous (E0154).

type struct Pt { v: i32; }
implement struct Pt { step(&this, u: i32) -> i64 { return (this.v + u) as i64; } }

const p: Pt = Pt { v: 5 };
const f = Pt::step;                      // (&Pt, i32) -> i64
const n: i64 = f(&p, 7);                 // 12

The impl-qualified path. (Tr for T)::m names the m that Tr's implementation for T delivers, whatever other traits provide m for T. It is one rule for every shape the tie takes: a static method, (Tr for T)::m(), which no receiver could select; a method with a receiver, (Tr for T)::m(&recv, ...), the receiver its first argument; a method taken as a value, (Tr for T)::m; and the form inside a generic body, (Tr for U)::m() with U a type parameter, which selects Tr's implementation for whatever U is instantiated with. The head reuses for exactly as implement trait Tr for T writes it - a trait, then a type by name with generic arguments if any - and is followed by :: and one member. A head whose type does not implement the trait is refused (E0306, the bound the head asserts), one whose implementation has no such member is refused at the member (E0233), and the call's arguments are checked against that implementation's signature. A generic method's own type parameters bind from the path's turbofish, (Tr for T)::m::<u8>(&recv, x), or from the arguments after the receiver, as they do for recv.m(x). The trait-qualified call Tr::m(&recv, ...) also resolves, by the receiver's type, but only where there is a receiver; the impl-qualified path is the form E0156 proposes.

type trait Beta  { go(&this) -> i32; static make() -> i32; }
type trait Gamma { go(&this) -> i32; static make() -> i32; }
type struct P { v: i32; }
implement trait Beta  for P { go(&this) -> i32 { return 2; } static make() -> i32 { return 2; } }
implement trait Gamma for P { go(&this) -> i32 { return 5; } static make() -> i32 { return 5; } }

const p: P = P { v: 1 };
const a: i32 = P::make();               // error[E0156]: two traits provide `make`
const b: i32 = (Beta for P)::make();    // 2
const c: i32 = (Gamma for P)::go(&p);   // 5
const f: () -> i32 = (Gamma for P)::make;