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

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 through PartialEq; </<=/>/>= through PartialOrd. Numbers, str, and bool implement them in std.
  • Eq marks an equality that is total. The integers, str and bool are Eq (the floats are not: NaN), and List<T>, Option<T>, Result<T, E>, HashSet<T> and HashMap<K, V> are Eq when what they hold is, so a T: Eq takes [1, 2], Some(true) and Ok(1).
  • A tuple is PartialEq when its elements are, compared position by position at any arity: (1, "a") == (1, "a"), and a T: PartialEq takes a pair; it is Eq when its elements are. Ordering a tuple is not provided.
  • For your own types, [derive(PartialEq)] gives structural equality, the usual path. Implement PartialOrd/Ord by hand when ordering is meaningful (Instant does this in std).
  • The B = Self parameter 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:

TraitOperatorTraitOperator
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 Callable must give T a call method — here or in a separate impl T block. The marker alone calls nothing, and the refusal is at the impl rather than at every call site.
  • A call method without the marker stays an ordinary method. Calling such a value is refused, with the impl named as the fix.
  • A Callable in a FIELD is called through the field — (a.b)(c), never a.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.