Core traits reference
The traits behind the operators and the derive set: std::compare,
std::default, std::operators.
std::compare
trait PartialEq<B = Self> {
fun eq(self, b: B): bool; // ==
fun ne(self, b: B): bool; // != (default: !eq)
}
trait Eq with PartialEq {}
enum Ordering { Less, Equal, Greater }
trait PartialOrd<B = Self> with PartialEq<B> {
fun partial_compare(self, b: B): Option<Ordering>;
fun lt(self, b: B): bool; // < (defaults over partial_compare)
fun le(self, b: B): bool; // <=
fun gt(self, b: B): bool; // >
fun ge(self, b: B): bool; // >=
}
trait Ord with Eq + PartialOrd {
fun compare(self, b: Self): Ordering;
fun min(self, b: Self): Self;
fun max(self, b: Self): Self;
fun clamp(self, min: Self, max: Self): Self;
}
==/!=dispatch throughPartialEq;</<=/>/>=throughPartialOrd. Numbers,str, andboolimplement them in std.Eqmarks an equality that is total. The integers,strandboolareEq(the floats are not: NaN), andList<T>,Option<T>,Result<T, E>,HashSet<T>andHashMap<K, V>areEqwhen what they hold is, so aT: Eqtakes[1, 2],Some(true)andOk(1).- A tuple is
PartialEqwhen its elements are, compared position by position at any arity:(1, "a") == (1, "a"), and aT: PartialEqtakes a pair; it isEqwhen its elements are. Ordering a tuple is not provided. - For your own types,
[derive(PartialEq)]gives structural equality, the usual path. ImplementPartialOrd/Ordby hand when ordering is meaningful (Instantdoes this in std). - The
B = Selfparameter allows cross-type comparison impls; you’ll rarely need it.
std::default
trait Default {
fun default(): Self;
}
Zero for numbers, "" for str, false for bool, None for
Option<T>, the empty container for List<T>, HashMap<K, V> and
HashSet<T>, and the element’s default at every position for a fixed
array [T; n] ([i32; 4]::default()). default_of<T>() is T::default()
spelled as a generic call, default_of<[i32; 4]>(). [derive(Default)] composes fields’ defaults — so a struct
holding a container derives one. Used as a bound by helpers like
unwrap_or_default and List.sum.
Each impl lives in its own type’s module (Option’s in std::option,
List’s in std::list, and so on); std::default itself carries only
the two scalars that have no module of their own.
std::operators: the operator traits
Each operator dispatches through a trait; implement the trait, get the operator:
| Trait | Operator | Trait | Operator | |
|---|---|---|---|---|
Add<B = Self> | + | Shl | << | |
Sub | - | Shr | >> | |
Mul | * | BitAnd | & | |
Div | / | BitOr | | | |
Rem | % | BitXor | ^ |
The B = Self parameter types the right-hand side; mixed-operand impls
are how std’s Instant + Duration works:
import std::operators::Add;
struct Celsius {
degrees: f64,
}
impl Celsius with Add {
fun add(self, b: Celsius): Celsius {
Celsius { degrees = self.degrees + b.degrees }
}
}
fun main() {
let morning = Celsius { degrees = 20.5 };
let rise = Celsius { degrees = 1.5 };
print((morning + rise).degrees);
}
Compound assignment (+=, /=, …) rides the same impls. The
operator’s result type is Self (the left operand’s type).
std::operators: Try and Lift
The machinery behind !, ?. and ?
(control flow):
enum Verdict<T, B> { Good(T), Bad(B) }
trait Try<T, B> {
fun verdict(self): Verdict<T, B>; // split into good/bad
fun from_bad(bad: B): Self; // rebuild from the bad half (for propagation)
}
trait Lift {} // opt-in marker for ? and ?.
Option and Result implement both in std. A custom two-outcome type that
implements Try gets !; adding the Lift marker gets both lift forms —
the ?. chain and the bare ? that lifts a whole expression.
Lift declares no members: it is consent, not a contract you fill in. What
the operators actually call is a pair of ordinary methods your container
supplies — map<U>(self, |T| U) for the plain case, and
and_then<U>(self, |T| Self-of-U) for the flattening one. The element is the
container’s first type argument.
import std::operators::Lift;
struct Tagged<T> {
value: T,
tag: str,
}
impl Tagged<type T> with Lift {}
impl Tagged<type T> {
fun map<U>(self, fn: |T| U): Tagged<U> {
Tagged { value = fn(self.value), tag = self.tag }
}
fun and_then<U>(self, fn: |T| Tagged<U>): Tagged<U> {
let inner = fn(self.value);
Tagged { value = inner.value, tag = self.tag + "+" + inner.tag }
}
}
fun main() {
let price = Tagged { value = 40, tag = "eur" };
let tax = Tagged { value = 2, tag = "eur" };
let doubled = price? * 2; // map — Tagged<i32>, value 80
let total = price? + tax?; // and_then, then map — value 42
print(total.tag); // eur+eur
}
A region with several ?s nests the calls left to right — price.and_then(|p| tax.map(|t| p + t)) above — so short-circuiting and laziness are whatever
your and_then does with the closure it is handed. Every receiver in one
expression must be the same container; a type with a map but no Lift is
refused, and a missing map/and_then is named in the error.
std::operators: Callable
Callable is the opt-in marker for the call operator: a value whose
type implements it is called like a function, and value(args) resolves
as the method call on it.
trait Callable {} // opt-in marker for value(args)
The marker declares no members. The call method the impl writes carries
the arity and the types, so everything the call checks — argument count,
argument types, generic binding, a context clause — is the ordinary
method check, and value(args) and value.call(args) are the same call:
import std::operators::Callable;
struct Scale {
unit: f64,
}
impl Scale with Callable {
fun call(self, n: f64): f64 {
self.unit * n
}
}
fun main() {
let space = Scale { unit = 0.25 };
print(space(2f)); // 0.5
print(space.call(2f)); // 0.5 — the same call
let step: |f64| f64 = space; // a Callable coerces into a closure slot
print(step(8f)); // 2
}
Three rules complete it:
impl T with Callablemust giveTacallmethod — here or in a separateimpl Tblock. The marker alone calls nothing, and the refusal is at the impl rather than at every call site.- A
callmethod without the marker stays an ordinary method. Calling such a value is refused, with the impl named as the fix. - A
Callablein a FIELD is called through the field —(a.b)(c), nevera.b(c), which is method lookup. This is the rule a closure-valued field already has, for the same reason.
A Callable value also coerces wherever a function declaration does
(let f: |f64| f64 = space;, list.map(space)): the slot takes it, and it
is lowered as the wrapping closure |n| space.call(n). A struct TYPE name
is not a value, so Scale(0.25) is still the construction refusal it
always was.