11Traits
A trait names a set of methods that a type may implement. Traits are how generic code expresses requirements on its type parameters and how the standard library models capabilities such as equality, ordering, hashing, formatting, and I/O.
11.1 Declaring a Trait
type trait Eq {
equals(&this, other: &This) -> boolean;
}
Inside the trait body, This refers to the implementing type. Methods may have default bodies:
type trait Read {
read(mut &this, buf: u8*, len: u64) -> Result<u64, IoError>;
/// Default: keep reading until end-of-stream or error.
read_all(mut &this, out: mut &Array<u8>) -> Result<u64, IoError> {
// ... default implementation calls self.read in a loop ...
}
}
A trait may inherit from a base trait; implementations of the derived trait must also implement the base:
type trait Ord : Eq {
compare(&this, other: &This) -> Ordering;
}
11.2 Implementing a Trait
implement trait <Trait> for <Type> { ... } provides the method bodies for a concrete type.
implement trait Eq for i32 {
equals(&this, other: &i32) -> boolean {
return this == *other;
}
}
implement trait Ord for i32 {
compare(&this, other: &i32) -> Ordering {
if (this < *other) { return Ordering::Less; }
if (this > *other) { return Ordering::Greater; }
return Ordering::Equal;
}
}
You may implement a trait for any type defined in the same crate, including primitive types.
11.3 Trait Bounds with where
A generic parameter is constrained to types that implement specific traits via a where clause:
function smallest<T>(xs: &Array<T>) -> Option<T>
where T: Ord + Clone {
if (xs.length() == 0) { return Option::None; }
mut best: T = xs.get(0).clone();
for (mut i: u64 = 1; i < xs.length(); i++) {
const next: T = xs.get(i).clone();
if (next.compare(&best) == Ordering::Less) {
best = next;
}
}
return Option::Some(best);
}
Multiple bounds on the same parameter are joined with +. Multiple constrained parameters are separated with ,:
where T: Hash + Eq, V: Clone
11.4 Standard Library Traits
| Trait | Purpose |
|---|---|
Copy | Marker for types that are bitwise-copyable (no Drop impl). |
Drop | Explicit destructor; types implementing it are non-Copy. |
Clone | Explicit deep duplication via clone(). |
Default | A canonical zero value: static default() -> This. |
Eq | Equality (backs == / !=; see section 11.6). |
Ord (: Eq) | Total ordering via compare(...) -> Ordering (backs < > <= >=). |
Hash | Type-level hashing into a Hasher. |
Add/Sub/Mul/Div/Rem, Neg, BitAnd/BitOr/BitXor/Shl/Shr, Not/BitNot, Index, Deref | Operator overloading - see section 11.6. |
Iterator | Lazy sequence with an associated Item and next() -> Option<Item> (see section 11.5). |
IntoIterator | Conversion into an iterator. |
From<T> / Into<T> | Infallible conversions. |
TryFrom<T> / TryInto<T> | Fallible conversions returning Result. |
Read / Write | Byte-level I/O. |
Display / Debug | Formatting. |
FmtWrite | Sink trait for the formatter. |
Allocator | Heap allocation strategy. |
Every standard-library trait is declared in stdlib/core/ with the exception of Read/Write (in stdlib/io/traits.cryo), Display/Debug/FmtWrite (in stdlib/fmt/), and Allocator (in stdlib/alloc/allocator.cryo).
The collection iterator entry points (Array::iter, HashMap::keys/values, Str::split, ...) return implement Iterator<...> rather than naming their concrete cursor structs, so you consume them through the trait and a for-in loop without ever spelling the underlying type.
11.5 Associated Types
A trait may declare an associated type - a type that each implementation
supplies, named once in the trait and referred to by every method. The
standard Iterator is written this way:
type trait Iterator {
type Item; // each impl chooses its element type
next(mut &this) -> Option<This::Item>; // `This::Item` projects it
// ... defaults (count, fold, map, filter, ...) all in terms of This::Item ...
}
This::Item is a projection: inside the trait it stands for "the Item
this implementation bound". Projections also work off a generic parameter - a
generic adapter names its source's element as I::Item:
type struct MapIter<I, O> { inner: I; f: (I::Item) -> O; }
Binding the associated type. An implementation binds each associated type in one of two ways:
// 1. Positional sugar - `Iterator<i32>` == `Iterator<Item = i32>`. Available
// only when the trait has no generic params of its own (Iterator's case).
implement trait Iterator<i32> for struct Counter { ... }
// 2. Explicit body form - always available, and required when a value
// expression rather than sugar is clearer.
implement trait Iterator for struct Counter {
type Item = i32;
...
}
The positional form scales to where-clause adapters, whose element flows from
the source: implement<I, A> trait Iterator<A> for struct TakeIter<I> where I: Iterator<A> binds Item := A := I::Item.
Declaration-site bounds. An associated type may carry a bound
(type Item: Copy;). Every impl's concrete binding is checked against it - an
impl whose Item does not satisfy the bound is rejected with E0306:
type trait Seq { type Item: Copy; next_one(&this) -> i32; }
implement trait Seq<NotCopy> for struct Holder { ... } // E0306: Item not Copy
Diagnostics. Three errors guard the rules:
E0306- an impl binds anItemthat does not satisfy a declaration-site bound (type Item: Copy;).E0309- an impl of a trait that declares an associated type binds none of them (no positional arg and notype Item = ...;body). The projection could never reduce, so it is rejected up front.E0310- an associated type bound positionally on a trait that also has generic parameters. Positional args fill the declared generic params in order, so an associated type of such a trait must be bound with the explicit body form (type Out = ...;).
Because Cryo monomorphises, a projection is fully resolved at compile time:
MapIter<Range<i32>, i64>::Item reduces to i64 with no runtime cost.
11.6 Operator Overloading
Operators on a user-defined type desugar, in the compiler, to a call to the corresponding operator trait method. Implement the trait and the operator works on your type; the rewrite happens during semantic analysis, so there is no runtime dispatch and the result monomorphises like any other method call.
The rewrite is type-directed and LHS-driven: a OP b dispatches on the
type of a, and it only fires when the built-in rule does not already apply.
Primitive arithmetic (1 + 2), pointer stepping, and native integer/pointer
comparison keep emitting raw instructions - primitives deliberately do not
implement the arithmetic traits. The operator traits live in
stdlib/core/ops.cryo (Eq/Ord are in
stdlib/core/cmp.cryo).
| Operator(s) | Trait (core::ops / core::cmp) | Method / desugar |
|---|---|---|
+ - * / % | Add Sub Mul Div Rem | a + b -> a.add(&b) (etc.) |
- (unary) | Neg | -a -> a.neg() |
& | ^ << >> | BitAnd BitOr BitXor Shl Shr | a & b -> a.bitand(&b) (etc.) |
! ~ (unary) | Not BitNot | !a -> a.not(), ~a -> a.bitnot() |
== != | Eq | a == b -> a.equals(&b), a != b -> !a.equals(&b) |
< > <= >= | Ord (: Eq) | a < b -> a.compare(&b).is_lt() (is_gt/is_le/is_ge) |
a[i] | Index<Idx, Output> | a[i] -> *(a.index(i)) |
*a (deref) | Deref<Target> | *a -> *(a.deref()) |
Each arithmetic/bitwise trait carries Rhs and Output type parameters, so an
operator can mix types (add a scalar to a vector, shift by a plain integer) and
choose its result type. The right operand is taken by reference (rhs: &Rhs)
to avoid moving an owned aggregate.
type struct Vec2 { x: i64; y: i64; }
implement trait Add<Vec2, Vec2> for struct Vec2 {
add(&this, rhs: &Vec2) -> Vec2 { return Vec2 { x: this.x + rhs.x, y: this.y + rhs.y }; }
}
implement trait Mul<i64, Vec2> for struct Vec2 { // scalar on the right
mul(&this, rhs: &i64) -> Vec2 { return Vec2 { x: this.x * *rhs, y: this.y * *rhs }; }
}
implement trait Neg<Vec2> for struct Vec2 {
neg(&this) -> Vec2 { return Vec2 { x: -this.x, y: -this.y }; }
}
const a: Vec2 = Vec2 { x: 1, y: 2 };
const b: Vec2 = Vec2 { x: 3, y: 4 };
const c: Vec2 = a + b; // Vec2 { 4, 6 }
const d: Vec2 = a * 3; // Vec2 { 3, 6 }
const e: Vec2 = -a; // Vec2 { -1, -2 }
Equality and ordering. Implement Eq for ==/!= and Ord (which extends
Eq) for < > <= >=. A single compare backs all four relational operators
through the Ordering predicates:
implement trait Eq for struct Vec2 {
equals(&this, other: &Vec2) -> boolean { return this.x == other.x && this.y == other.y; }
}
implement trait Ord for struct Vec2 {
compare(&this, other: &Vec2) -> Ordering {
if (this.x != other.x) { return this.x.compare(&other.x); }
return this.y.compare(&other.y);
}
}
const eq: boolean = a == b; // a.equals(&b) -> false
const lt: boolean = a < b; // a.compare(&b).is_lt() -> true
Compound assignment routes through the same trait: a += b evaluates
a.add(&b) and stores the result back into a. This holds for every binary
arithmetic, bitwise, and shift operator (+= -= *= /= %= &= |= ^= <<= >>=).
Indexing. Index<Idx, Output>::index returns a pointer (Output*), so
the desugar a[i] -> *(a.index(i)) is a place: one implementation serves
reads (v = a[i]), writes (a[i] = v), and compound assignment (a[i] += v).
Built-in array/slice/string/pointer indexing keeps its native path.
type struct Grid { cells: i32[9]; }
implement trait Index<u64, i32> for struct Grid {
index(&this, i: u64) -> i32* { return &this.cells[i]; }
}
mut g: Grid = ...;
g[0] = 42; // *(g.index(0)) = 42
g[0] += 1; // index called once
Dereference and auto-deref. Deref<Target>::deref also returns a pointer
(Target*), so *b -> *(b.deref()) is likewise a place (read/write/compound).
Member access coerces through Deref too: when a receiver lacks a field or
method but implements Deref, b.field / b.method() retry on the pointee
(transitively), inserting the .deref() chain for you - the smart-pointer
pattern.
type struct Boxed<T> { value: T; }
implement<T> trait Deref<T> for struct Boxed<T> {
deref(&this) -> T* { return &this.value; }
}
mut b: Boxed<Vec2> = Boxed<Vec2> { value: Vec2 { x: 1, y: 2 } };
const v: Vec2 = *b; // *(b.deref())
b.x = 10; // auto-deref: (*b.deref()).x - b has no field `x`
Reference operands. A by-reference operand (&T, the common
container/parameter case) overloads exactly like a value - v[i], a + b,
-a, and a == b all work when a/v is a &T. In particular &x == &y
(where the referent implements Eq) compares by value (x.equals(&y)),
not by pointer identity. A raw pointer T* is never unwrapped this way: p == null and pointer arithmetic stay on the primitive path.
Reflected (primitive left operand). When the left operand is a primitive and
the right is a user type, the operator dispatches to a trait implemented on
the primitive - 2 * v calls (2).mul(&v) given implement trait Mul<Vec2, Vec2> for i64. This is the one case where the right operand's type pulls in the
impl; Eq/Ord are excluded (their operands share This).
Generic and bounded-param dispatch work throughout: a T: Add parameter
overloads a + b in a generic body (Phase 2), and the desugar is carried through
monomorphisation so Container<i32>::index (etc.) specialises correctly. When a
type lacks the required impl, the compiler names the missing trait (e.g. "Vec2
does not implement Mul; add implement trait Mul ... for Vec2").