Generic Traits, Operator Overloading and the Prelude
Traits with type parameters, default type parameters like Rhs = Self, overloading + and * with std::ops, and what the prelude imports for you.
You want point_a + point_b to just work for your own type. In Rust, + is a call to a trait method, and that trait is generic. Once you understand generic traits, operator overloading is a small step, and so is understanding why some traits are always in scope without a use.
Generic traits
A trait can take a type parameter, just like a struct:
trait Convert<T> {
fn convert(&self) -> T;
}
struct Celsius(f64);
impl Convert<f64> for Celsius {
fn convert(&self) -> f64 {
self.0 * 9.0 / 5.0 + 32.0
}
}
impl Convert<String> for Celsius {
fn convert(&self) -> String {
format!("{:.1}°C", self.0)
}
}
fn main() {
let t = Celsius(21.5);
let f: f64 = t.convert();
let s: String = t.convert();
println!("{f} / {s}");
}
Because Convert<f64> and Convert<String> are different traits, one type can implement both. The type annotation on f and s picks which one runs.
Generic trait or associated type?
Compare with an associated type, as in Iterator:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
- Associated type: one implementation per type. A
Vec<i32>iterator yields one kind of item. Callers don't have to name it. - Generic parameter: many implementations per type.
Celsiusconverts to several targets.
If a type should only ever have one answer, use an associated type. If it can have several, use a generic parameter.
Default type parameters
A generic parameter can have a default. The standard Add trait looks like this:
trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
Rhs = Self means "if you don't say otherwise, the right-hand side is the same type". So impl Add for Point means impl Add<Point> for Point. It has both a generic parameter (Rhs, many possible) and an associated type (Output, one per impl).
Operator overloading with std::ops
Each operator maps to a trait in std::ops: + is Add, - is Sub, * is Mul, unary - is Neg, += is AddAssign, [] is Index, and so on. Implement the trait and the operator works.
use std::ops::{Add, AddAssign, Mul, Neg};
#[derive(Debug, Clone, Copy, PartialEq)]
struct Vec2 {
x: f64,
y: f64,
}
impl Add for Vec2 {
type Output = Vec2;
fn add(self, rhs: Vec2) -> Vec2 {
Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
}
}
// Scale by a number: Vec2 * f64. Rhs is f64 here, not Self.
impl Mul<f64> for Vec2 {
type Output = Vec2;
fn mul(self, k: f64) -> Vec2 {
Vec2 { x: self.x * k, y: self.y * k }
}
}
impl Neg for Vec2 {
type Output = Vec2;
fn neg(self) -> Vec2 {
Vec2 { x: -self.x, y: -self.y }
}
}
impl AddAssign for Vec2 {
fn add_assign(&mut self, rhs: Vec2) {
self.x += rhs.x;
self.y += rhs.y;
}
}
fn main() {
let a = Vec2 { x: 1.0, y: 2.0 };
let b = Vec2 { x: 0.5, y: -1.0 };
let mut c = a + b * 2.0;
c += -a;
println!("{c:?}"); // Vec2 { x: 1.0, y: -2.0 }
}
A few things to note:
Add::addtakesselfby value. That's fine for smallCopytypes. For big types like matrices, also implement it for references (impl Add<&Matrix> for &Matrix) so&m1 + &m2doesn't move anything.Vec2 * f64works, butf64 * Vec2doesn't until you also writeimpl Mul<Vec2> for f64.- Comparison operators (
==,<) come fromPartialEqandPartialOrdinstd::cmp, usually derived. - Keep operators unsurprising.
+on a vector should add. If the meaning isn't obvious, use a named method.
The prelude
You used Vec, String, Option, Some, Clone and Iterator without importing them. That's because every module gets an automatic use std::prelude::rust_2021::*;. It brings in the types and traits almost every program needs, including:
- Types and variants:
Option,Some,None,Result,Ok,Err,Box,String,Vec - Traits:
Clone,Copy,Drop,Default,PartialEq,Eq,PartialOrd,Ord,Iterator,IntoIterator,Extend,From,Into,ToString,AsRef,AsMut,Fn,FnMut,FnOnce,Send,Sync,Sized - New in the 2021 edition:
TryFrom,TryInto,FromIterator
Trait methods only work when the trait is in scope. That's why x.clone() just works, but a + b on your own type needs the impl Add, and calling Add::add(a, b) directly needs use std::ops::Add;. The operator traits are not in the prelude. The compiler finds them for the operator syntax itself, but you import them to write the impl.
Crates often ship their own prelude, such as use rayon::prelude::*;. That's a regular module of re-exports the author picked, so one line brings in the traits you need.
Summary
- A generic trait (
Trait<T>) can be implemented many times for one type. An associated type allows one implementation. - Default type parameters (
Rhs = Self) keep the common case short. - Operators are trait methods from
std::ops. ImplementAdd,Mul,Neg,AddAssignand friends to overload them. - The prelude imports the everyday types and traits for you. Operator traits are not among them, so import them to implement them.