Lesson 34 of 94
Advanced Traits
This lesson covers the trait features you meet in real libraries: associated types (the Item in Iterator<Item = T>), operator overloading, supertraits, calling a specific trait's method when names clash, and the newtype pattern for working around the orphan rule.
1. Associated types
An associated type is a type chosen by each implementation, declared with type Name; in the trait. The standard Iterator trait uses one: type Item; says what the iterator yields.
trait Container {
type Item;
fn get(&self, index: usize) -> Option<&Self::Item>;
fn first(&self) -> Option<&Self::Item> {
self.get(0)
}
}
struct Playlist {
songs: Vec<String>,
}
struct Scores {
values: Vec<u32>,
}
impl Container for Playlist {
type Item = String;
fn get(&self, index: usize) -> Option<&String> {
self.songs.get(index)
}
}
impl Container for Scores {
type Item = u32;
fn get(&self, index: usize) -> Option<&u32> {
self.values.get(index)
}
}
fn main() {
let p = Playlist { songs: vec![String::from("Intro"), String::from("Outro")] };
let s = Scores { values: vec![98, 87] };
println!("{:?} {:?}", p.first(), p.get(1));
println!("{:?} {:?}", s.first(), s.get(5));
}Some("Intro") Some("Outro")
Some(98) None2. Implementing Iterator with its associated type
Iterator has one required method, next, which returns Option<Self::Item>. Custom Iterators goes much deeper; this is the minimum.
struct Countdown {
remaining: u32,
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.remaining == 0 {
return None;
}
self.remaining -= 1;
Some(self.remaining + 1)
}
}
fn main() {
for n in (Countdown { remaining: 3 }) {
print!("{n}... ");
}
println!("liftoff");
let total: u32 = Countdown { remaining: 10 }.sum(); // one of Iterator's ~70 default methods
println!("sum = {total}");
}3... 2... 1... liftoff
sum = 553. Associated types vs generic type parameters
A generic trait (trait Convert<T>) can be implemented many times for one type, once per T. A trait with an associated type can be implemented only once per type, so the type is fixed. Use an associated type when each implementing type has exactly one natural choice, as with Iterator::Item.
// Generic trait: one type can implement it for many targets.
trait ConvertTo<T> {
fn convert(&self) -> T;
}
struct Celsius(f64);
impl ConvertTo<f64> for Celsius {
fn convert(&self) -> f64 {
self.0
}
}
impl ConvertTo<String> for Celsius {
fn convert(&self) -> String {
format!("{:.1}°C", self.0)
}
}
// Associated type: exactly one output per implementing type.
trait Parse {
type Output;
fn parse(input: &str) -> Option<Self::Output>;
}
struct Port;
impl Parse for Port {
type Output = u16;
fn parse(input: &str) -> Option<u16> {
input.parse().ok().filter(|&p| p >= 1)
}
}
fn main() {
let t = Celsius(21.456);
let as_number: f64 = t.convert();
let as_text: String = t.convert();
println!("{as_number} / {as_text}");
println!("{:?} {:?}", Port::parse("8080"), Port::parse("0"));
}21.456 / 21.5°C
Some(8080) NoneWith the generic trait, the caller had to say which conversion it wanted (through the variable's type). With the associated type, there's nothing to choose.
4. Constraining an associated type in a bound
I: Iterator<Item = u32> means "any iterator whose Item is u32".
fn total_bytes<I: Iterator<Item = u64>>(sizes: I) -> u64 {
let mut total = 0;
for s in sizes {
total += s;
}
total
}
fn main() {
let files = vec![1_024_u64, 2_048, 512];
println!("{}", total_bytes(files.into_iter()));
println!("{}", total_bytes((1..=4).map(|kb| kb * 1_000)));
}3584
100005. -> impl Trait in trait methods
A trait method can return impl Trait (since Rust 1.75). Each implementation then returns its own concrete type, and callers only see the trait. This is the usual way to return an iterator from a trait method without boxing it:
use std::collections::HashMap;
trait Catalog {
fn product_names(&self) -> impl Iterator<Item = &str>;
// Default methods can use it like any iterator.
fn count_starting_with(&self, prefix: &str) -> usize {
self.product_names().filter(|name| name.starts_with(prefix)).count()
}
}
struct ListCatalog {
products: Vec<(String, bool)>, // (name, in stock)
}
struct MapCatalog {
prices: HashMap<String, u32>,
}
impl Catalog for ListCatalog {
// A Filter<Map<...>> type, never written out.
fn product_names(&self) -> impl Iterator<Item = &str> {
self.products.iter().filter(|(_, in_stock)| *in_stock).map(|(name, _)| name.as_str())
}
}
impl Catalog for MapCatalog {
// A different type: a Map over HashMap keys.
fn product_names(&self) -> impl Iterator<Item = &str> {
self.prices.keys().map(String::as_str)
}
}
fn main() {
let list = ListCatalog {
products: vec![("lamp".into(), true), ("ladder".into(), false), ("mug".into(), true)],
};
let map = MapCatalog { prices: HashMap::from([("laptop".to_string(), 999), ("lens".to_string(), 250)]) };
println!("{:?}", list.product_names().collect::<Vec<_>>());
let mut names: Vec<&str> = map.product_names().collect();
names.sort();
println!("{names:?}");
println!("{} {}", list.count_starting_with("l"), map.count_starting_with("l"));
}["lamp", "mug"]
["laptop", "lens"]
1 2Like generic methods, a method returning impl Trait makes the trait unusable as dyn Catalog. When you need dyn, return Box<dyn Iterator<Item = &str> + '_> instead. async fn in traits, covered in Async Traits, is built on exactly this feature: it returns impl Future.
6. Supertraits
trait Entity: Debug + Clone means "to implement Entity, a type must also implement Debug and Clone". Code using Entity can then rely on those traits too.
use std::fmt::Debug;
trait Entity: Debug + Clone {
fn id(&self) -> u64;
fn snapshot(&self) -> (u64, Self) {
(self.id(), self.clone())
}
}
#[derive(Debug, Clone)]
struct Customer {
id: u64,
name: String,
}
impl Entity for Customer {
fn id(&self) -> u64 {
self.id
}
}
fn audit<E: Entity>(e: &E) {
let (id, copy) = e.snapshot();
println!("audited #{id}: {:?}", copy); // Debug is guaranteed by the supertrait
}
fn main() {
audit(&Customer { id: 7, name: String::from("Acme") });
}audited #7: Customer { id: 7, name: "Acme" }7. Implementing a trait without its supertrait fails
trait Named {
fn name(&self) -> String;
}
trait Greeter: Named {
fn greet(&self) -> String {
format!("Hello, {}", self.name())
}
}
struct Robot;
impl Greeter for Robot {}
fn main() {}error[E0277]: the trait bound `Robot: Named` is not satisfied
--> src/main.rs:13:18
|
13 | impl Greeter for Robot {}
| ^^^^^ unsatisfied trait bound
|
help: the trait `Named` is not implemented for `Robot`
--> src/main.rs:11:1
|
11 | struct Robot;
| ^^^^^^^^^^^^
help: this trait has no implementations, consider adding one
--> src/main.rs:1:1
|
1 | trait Named {
| ^^^^^^^^^^^
note: required by a bound in `Greeter`
--> src/main.rs:5:16
|
5 | trait Greeter: Named {
| ^^^^^ required by this bound in `Greeter`This is not inheritance. A supertrait is a requirement: Robot must implement Named separately, with its own impl block.
8. Operator overloading: Add
Every operator is a trait in std::ops. Implementing Add makes a + b work for your type. The associated type Output says what the result is.
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Money {
cents: i64,
}
impl Add for Money {
type Output = Money;
fn add(self, other: Money) -> Money {
Money { cents: self.cents + other.cents }
}
}
fn main() {
let price = Money { cents: 1_999 };
let shipping = Money { cents: 499 };
let total = price + shipping;
println!("{:?}", total);
println!("{}", total == Money { cents: 2_498 });
}Money { cents: 2498 }
trueRust lets domain types read like arithmetic.
9. Adding different types: Add<Rhs>
Add has a type parameter for the right-hand side, which defaults to Self. Implement Add<i64> to allow Money + i64.
use std::ops::Add;
#[derive(Debug, Clone, Copy)]
struct Money {
cents: i64,
}
impl Add<i64> for Money {
type Output = Money;
fn add(self, cents: i64) -> Money {
Money { cents: self.cents + cents }
}
}
fn main() {
let tip = Money { cents: 1_000 } + 250;
println!("{:?}", tip);
}Money { cents: 1250 }The trait is declared as trait Add<Rhs = Self>. Rhs = Self is a default type parameter, which is why plain impl Add for Money means impl Add<Money> for Money.
10. Sub, Mul, and Neg
use std::ops::{Mul, Neg, Sub};
#[derive(Debug, Clone, Copy, PartialEq)]
struct Vec2 {
x: f64,
y: f64,
}
impl Sub for Vec2 {
type Output = Vec2;
fn sub(self, o: Vec2) -> Vec2 {
Vec2 { x: self.x - o.x, y: self.y - o.y }
}
}
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 }
}
}
fn main() {
let target = Vec2 { x: 10.0, y: 4.0 };
let position = Vec2 { x: 2.0, y: 1.0 };
let direction = target - position;
println!("{:?}", direction);
println!("{:?}", direction * 0.5);
println!("{:?}", -direction);
}Vec2 { x: 8.0, y: 3.0 }
Vec2 { x: 4.0, y: 1.5 }
Vec2 { x: -8.0, y: -3.0 }11. Compound assignment: AddAssign
+= is a separate trait, AddAssign, which changes the left side in place.
use std::ops::AddAssign;
#[derive(Debug, Default)]
struct Totals {
orders: u32,
revenue_cents: u64,
}
impl AddAssign<u64> for Totals {
fn add_assign(&mut self, order_cents: u64) {
self.orders += 1;
self.revenue_cents += order_cents;
}
}
fn main() {
let mut t = Totals::default();
for cents in [1_999, 4_500, 250] {
t += cents;
}
println!("{:?}", t);
}Totals { orders: 3, revenue_cents: 6749 }12. Indexing with Index and IndexMut
container[key] calls Index::index, and assignment through it calls IndexMut::index_mut. Here a grid is indexed by (row, column) tuples.
use std::ops::{Index, IndexMut};
struct Grid {
width: usize,
cells: Vec<char>,
}
impl Grid {
fn new(width: usize, height: usize) -> Self {
Self { width, cells: vec!['.'; width * height] }
}
}
impl Index<(usize, usize)> for Grid {
type Output = char;
fn index(&self, (row, col): (usize, usize)) -> &char {
&self.cells[row * self.width + col]
}
}
impl IndexMut<(usize, usize)> for Grid {
fn index_mut(&mut self, (row, col): (usize, usize)) -> &mut char {
&mut self.cells[row * self.width + col]
}
}
fn main() {
let mut g = Grid::new(4, 3);
g[(0, 0)] = 'S';
g[(2, 3)] = 'E';
g[(1, 1)] = '#';
for row in 0..3 {
let line: String = (0..4).map(|col| g[(row, col)]).collect();
println!("{line}");
}
println!("start is {}", g[(0, 0)]);
}S...
.#..
...E
start is S13. The Sum trait: making .sum() work for your type
Iterator's .sum() works for any type implementing std::iter::Sum.
use std::iter::Sum;
use std::ops::Add;
#[derive(Debug, Clone, Copy)]
struct Duration {
secs: u64,
}
impl Add for Duration {
type Output = Duration;
fn add(self, o: Duration) -> Duration {
Duration { secs: self.secs + o.secs }
}
}
impl Sum for Duration {
fn sum<I: Iterator<Item = Duration>>(iter: I) -> Duration {
iter.fold(Duration { secs: 0 }, |a, b| a + b)
}
}
fn main() {
let laps = vec![Duration { secs: 62 }, Duration { secs: 58 }, Duration { secs: 61 }];
let total: Duration = laps.iter().copied().sum();
println!("{:?} = {}m{}s", total, total.secs / 60, total.secs % 60);
}Duration { secs: 181 } = 3m1sfold combines all items into one value. Iterator Adapters covers it.
14. Operators on references
Implementing Add for Money consumes both operands. For non-Copy types, also implement it for references so &a + &b works without moving.
use std::ops::Add;
#[derive(Debug, Clone, PartialEq)]
struct Tags(Vec<String>);
impl Add for &Tags {
type Output = Tags;
fn add(self, other: &Tags) -> Tags {
let mut all = self.0.clone();
for t in &other.0 {
if !all.contains(t) {
all.push(t.clone());
}
}
Tags(all)
}
}
fn main() {
let a = Tags(vec![String::from("rust"), String::from("cli")]);
let b = Tags(vec![String::from("cli"), String::from("async")]);
let merged = &a + &b;
println!("{:?}", merged);
println!("originals intact: {:?} {:?}", a, b);
}Tags(["rust", "cli", "async"])
originals intact: Tags(["rust", "cli"]) Tags(["cli", "async"])15. Two traits with the same method name
A type can implement two traits that both define a method called name. Calling x.name() is then ambiguous, and Rust asks you to say which one you mean.
trait Pilot {
fn name(&self) -> String;
}
trait Wizard {
fn name(&self) -> String;
}
struct Human;
impl Pilot for Human {
fn name(&self) -> String { String::from("Captain") }
}
impl Wizard for Human {
fn name(&self) -> String { String::from("Merlin") }
}
fn main() {
println!("{}", Human.name());
}error[E0034]: multiple applicable items in scope
--> src/main.rs:20:26
|
20 | println!("{}", Human.name());
| ^^^^ multiple `name` found
|
note: candidate #1 is defined in an impl of the trait `Pilot` for the type `Human`
--> src/main.rs:12:5
|
12 | fn name(&self) -> String { String::from("Captain") }
| ^^^^^^^^^^^^^^^^^^^^^^^^
note: candidate #2 is defined in an impl of the trait `Wizard` for the type `Human`
--> src/main.rs:16:5
|
16 | fn name(&self) -> String { String::from("Merlin") }
| ^^^^^^^^^^^^^^^^^^^^^^^^
help: disambiguate the method for candidate #1
|
20 - println!("{}", Human.name());
20 + println!("{}", Pilot::name(&Human));
|
help: disambiguate the method for candidate #2
|
20 - println!("{}", Human.name());
20 + println!("{}", Wizard::name(&Human));
|16. Disambiguating with Trait::method(&x)
trait Pilot {
fn name(&self) -> String;
}
trait Wizard {
fn name(&self) -> String;
}
struct Human;
impl Pilot for Human {
fn name(&self) -> String {
String::from("Captain")
}
}
impl Wizard for Human {
fn name(&self) -> String {
String::from("Merlin")
}
}
impl Human {
fn name(&self) -> String {
String::from("Arthur")
}
}
fn main() {
let h = Human;
println!("{}", h.name()); // the inherent method wins
println!("{}", Pilot::name(&h));
println!("{}", Wizard::name(&h));
}Arthur
Captain
Merlin17. Fully qualified syntax for associated functions
Associated functions have no self to tell the traits apart, so you write the full form: <Type as Trait>::function().
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("{}", Dog::baby_name());
println!("{}", <Dog as Animal>::baby_name());
}Spot
puppy18. The newtype pattern: implementing foreign traits for foreign types
The orphan rule forbids impl Display for Vec<String>. Wrapping the Vec in your own tuple struct makes the type local, so any trait can be implemented for it.
use std::fmt;
struct Csv(Vec<String>);
impl fmt::Display for Csv {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let escaped: Vec<String> = self
.0
.iter()
.map(|field| if field.contains(',') { format!("\"{field}\"") } else { field.clone() })
.collect();
write!(f, "{}", escaped.join(","))
}
}
fn main() {
let row = Csv(vec![String::from("42"), String::from("Hopper, Grace"), String::from("admiral")]);
println!("{row}");
println!("{} fields", row.0.len());
}42,"Hopper, Grace",admiral
3 fieldsThe wrapper costs nothing at runtime. The downside is that Csv doesn't have Vec's methods; you reach them through .0, or by implementing Deref (Drop, Deref & Cow).
19. Newtypes to add meaning and restrict operations
A newtype can deliberately expose only a few operations. Here Meters can be added to Meters and scaled, but not added to Seconds.
use std::ops::{Add, Mul};
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Meters(f64);
#[derive(Debug, Clone, Copy)]
struct Seconds(f64);
impl Add for Meters {
type Output = Meters;
fn add(self, o: Meters) -> Meters {
Meters(self.0 + o.0)
}
}
impl Mul<f64> for Meters {
type Output = Meters;
fn mul(self, k: f64) -> Meters {
Meters(self.0 * k)
}
}
fn speed(distance: Meters, time: Seconds) -> f64 {
distance.0 / time.0
}
fn main() {
let leg1 = Meters(1_200.0);
let leg2 = Meters(800.0);
let total = leg1 + leg2;
println!("{:?}, doubled {:?}", total, total * 2.0);
println!("{:.2} m/s", speed(total, Seconds(600.0)));
// let nonsense = leg1 + Seconds(5.0); // does not compile
}Meters(2000.0), doubled Meters(4000.0)
3.33 m/s20. Adding meters to seconds is rejected
use std::ops::Add;
#[derive(Clone, Copy)]
struct Meters(f64);
#[derive(Clone, Copy)]
struct Seconds(f64);
impl Add for Meters {
type Output = Meters;
fn add(self, o: Meters) -> Meters { Meters(self.0 + o.0) }
}
fn main() {
let x = Meters(5.0) + Seconds(2.0);
}error[E0308]: mismatched types
--> src/main.rs:15:27
|
15 | let x = Meters(5.0) + Seconds(2.0);
| ----------- ^^^^^^^^^^^^ expected `Meters`, found `Seconds`
| |
| expected because this is `Meters`21. Marker traits
A marker trait has no methods. It labels types as having some property, and bounds can require it. Copy, Send, and Sync are standard marker traits.
trait Validated {}
#[derive(Debug)]
struct Email(String);
impl Validated for Email {}
#[derive(Debug)]
struct RawInput(String);
fn store<T: Validated + std::fmt::Debug>(value: T) {
println!("stored {:?}", value);
}
fn validate(input: RawInput) -> Option<Email> {
if input.0.contains('@') { Some(Email(input.0)) } else { None }
}
fn main() {
if let Some(email) = validate(RawInput(String::from("ana@example.com"))) {
store(email);
}
// store(RawInput(String::from("x"))); // does not compile: RawInput is not Validated
}stored Email("ana@example.com")22. Sealed traits
Sometimes a library wants a trait others can use but not implement, so new methods can be added later without breaking anyone. The trick: a public trait with a supertrait defined in a private module. Outside code can't name the private trait, so it can't implement the public one.
mod shapes {
mod private {
pub trait Sealed {}
}
pub trait Shape: private::Sealed {
fn sides(&self) -> u32;
}
pub struct Triangle;
pub struct Square;
impl private::Sealed for Triangle {}
impl private::Sealed for Square {}
impl Shape for Triangle {
fn sides(&self) -> u32 {
3
}
}
impl Shape for Square {
fn sides(&self) -> u32 {
4
}
}
}
use shapes::Shape;
fn main() {
println!("{} {}", shapes::Triangle.sides(), shapes::Square.sides());
}3 4Outside the shapes module, impl Shape for MyType fails because private::Sealed can't be implemented or even named.
23. Forwarding a trait through &T and Box<T>
If a function takes impl Describe, callers might have a &T or a Box<T>. Implementing the trait for those pointer types once makes them all work. The standard library does this for most of its traits.
trait Describe {
fn describe(&self) -> String;
}
struct Server(&'static str);
impl Describe for Server {
fn describe(&self) -> String {
format!("server {}", self.0)
}
}
impl<T: Describe + ?Sized> Describe for &T {
fn describe(&self) -> String {
(**self).describe()
}
}
impl<T: Describe + ?Sized> Describe for Box<T> {
fn describe(&self) -> String {
(**self).describe()
}
}
fn log(item: impl Describe) {
println!("{}", item.describe());
}
fn main() {
let s = Server("db-1");
log(&s);
log(Box::new(Server("db-2")));
log(s);
}server db-1
server db-2
server db-124. Realistic example: a complete Money type
A value type with arithmetic, comparison, display, and summing. Adding money in two different currencies is caught with a Result-returning method, while same-currency arithmetic reads naturally.
use std::fmt;
use std::iter::Sum;
use std::ops::{Add, Mul, Sub};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
struct Usd {
cents: i64,
}
impl Usd {
fn new(dollars: i64, cents: i64) -> Self {
Self { cents: dollars * 100 + cents }
}
}
impl Add for Usd {
type Output = Usd;
fn add(self, o: Usd) -> Usd {
Usd { cents: self.cents + o.cents }
}
}
impl Sub for Usd {
type Output = Usd;
fn sub(self, o: Usd) -> Usd {
Usd { cents: self.cents - o.cents }
}
}
impl Mul<i64> for Usd {
type Output = Usd;
fn mul(self, qty: i64) -> Usd {
Usd { cents: self.cents * qty }
}
}
impl Sum for Usd {
fn sum<I: Iterator<Item = Usd>>(iter: I) -> Usd {
iter.fold(Usd::default(), |a, b| a + b)
}
}
impl fmt::Display for Usd {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let sign = if self.cents < 0 { "-" } else { "" };
f.pad(&format!("{sign}${}.{:02}", self.cents.abs() / 100, self.cents.abs() % 100))
}
}
fn main() {
let lines = [(Usd::new(12, 50), 2), (Usd::new(3, 99), 3), (Usd::new(45, 0), 1)];
for (price, qty) in lines {
println!("{qty} x {price:>7} = {:>8}", price * qty);
}
let subtotal: Usd = lines.iter().map(|&(p, q)| p * q).sum();
let discount = Usd::new(10, 0);
let total = subtotal - discount;
println!("subtotal {subtotal:>9}");
println!("discount {:>9}", Usd { cents: -discount.cents });
println!("total {total:>9}");
println!("over $70? {}", total > Usd::new(70, 0));
}2 x $12.50 = $25.00
3 x $3.99 = $11.97
1 x $45.00 = $45.00
subtotal $81.97
discount -$10.00
total $71.97
over $70? true25. Realistic example: 2D vector math for a game
use std::ops::{Add, AddAssign, Mul, Sub};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
struct V2 {
x: f64,
y: f64,
}
impl Add for V2 {
type Output = V2;
fn add(self, o: V2) -> V2 {
V2 { x: self.x + o.x, y: self.y + o.y }
}
}
impl Sub for V2 {
type Output = V2;
fn sub(self, o: V2) -> V2 {
V2 { x: self.x - o.x, y: self.y - o.y }
}
}
impl Mul<f64> for V2 {
type Output = V2;
fn mul(self, k: f64) -> V2 {
V2 { x: self.x * k, y: self.y * k }
}
}
impl AddAssign for V2 {
fn add_assign(&mut self, o: V2) {
self.x += o.x;
self.y += o.y;
}
}
impl V2 {
fn length(self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
fn main() {
let gravity = V2 { x: 0.0, y: -9.8 };
let mut position = V2 { x: 0.0, y: 20.0 };
let mut velocity = V2 { x: 3.0, y: 0.0 };
let dt = 0.5;
for step in 1..=4 {
velocity += gravity * dt;
position += velocity * dt;
println!("t={:.1}s pos=({:.2}, {:.2}) speed={:.2}", step as f64 * dt, position.x, position.y, velocity.length());
}
let to_origin = V2::default() - position;
println!("distance to origin: {:.2}", to_origin.length());
}t=0.5s pos=(1.50, 17.55) speed=5.75
t=1.0s pos=(3.00, 12.65) speed=10.25
t=1.5s pos=(4.50, 5.30) speed=15.00
t=2.0s pos=(6.00, -4.50) speed=19.83
distance to origin: 7.5026. Realistic example: a sparse matrix with Index
A sparse matrix stores only non-zero entries. Index returns a reference, so for missing entries it returns a reference to a shared static zero.
use std::ops::Index;
struct Sparse {
rows: usize,
cols: usize,
entries: Vec<((usize, usize), f64)>,
}
static ZERO: f64 = 0.0;
impl Sparse {
fn set(&mut self, r: usize, c: usize, v: f64) {
assert!(r < self.rows && c < self.cols, "({r}, {c}) out of bounds");
self.entries.retain(|(pos, _)| *pos != (r, c));
if v != 0.0 {
self.entries.push(((r, c), v));
}
}
}
impl Index<(usize, usize)> for Sparse {
type Output = f64;
fn index(&self, pos: (usize, usize)) -> &f64 {
self.entries.iter().find(|(p, _)| *p == pos).map(|(_, v)| v).unwrap_or(&ZERO)
}
}
fn main() {
let mut m = Sparse { rows: 1_000, cols: 1_000, entries: Vec::new() };
m.set(0, 0, 1.5);
m.set(999, 42, -3.0);
m.set(0, 0, 0.0); // setting zero removes the entry
println!("{} {} {}", m[(0, 0)], m[(999, 42)], m[(500, 500)]);
println!("stored entries: {} of {}", m.entries.len(), m.rows * m.cols);
}0 -3 0
stored entries: 1 of 1000000assert! panics with the message if the condition is false: a check for bugs that is always enabled, even in release builds.
27. Realistic example: a key-value store trait with associated types
Each backend chooses its own key and value types, and generic code works with any backend.
use std::fmt::Debug;
trait Store {
type Key: Debug + PartialEq + Clone;
type Value: Debug + Clone;
fn put(&mut self, key: Self::Key, value: Self::Value);
fn get(&self, key: &Self::Key) -> Option<Self::Value>;
}
struct SessionStore {
sessions: Vec<(u64, String)>,
}
struct FeatureFlags {
flags: Vec<(&'static str, bool)>,
}
impl Store for SessionStore {
type Key = u64;
type Value = String;
fn put(&mut self, key: u64, value: String) {
self.sessions.retain(|(k, _)| *k != key);
self.sessions.push((key, value));
}
fn get(&self, key: &u64) -> Option<String> {
self.sessions.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
}
}
impl Store for FeatureFlags {
type Key = &'static str;
type Value = bool;
fn put(&mut self, key: &'static str, value: bool) {
self.flags.retain(|(k, _)| *k != key);
self.flags.push((key, value));
}
fn get(&self, key: &&'static str) -> Option<bool> {
self.flags.iter().find(|(k, _)| k == key).map(|(_, v)| *v)
}
}
fn copy_entry<S: Store>(store: &mut S, from: S::Key, to: S::Key) -> bool {
match store.get(&from) {
Some(v) => {
println!("copying {:?} -> {:?}: {:?}", from, to, v);
store.put(to, v);
true
}
None => false,
}
}
fn main() {
let mut sessions = SessionStore { sessions: vec![(1, String::from("user=ana"))] };
copy_entry(&mut sessions, 1, 2);
println!("{:?}", sessions.get(&2));
let mut flags = FeatureFlags { flags: vec![("new_checkout", true)] };
copy_entry(&mut flags, "new_checkout", "new_checkout_eu");
println!("{:?} {:?}", flags.get(&"new_checkout_eu"), flags.get(&"dark_mode"));
}copying 1 -> 2: "user=ana"
Some("user=ana")
copying "new_checkout" -> "new_checkout_eu": true
Some(true) NoneBounds on associated types (type Key: Debug + PartialEq + Clone;) let generic code like copy_entry print, compare, and clone keys without knowing their concrete types.
28. Realistic example: temperature units with conversions and comparison
use std::cmp::Ordering;
use std::fmt;
#[derive(Debug, Clone, Copy)]
enum Temp {
C(f64),
F(f64),
K(f64),
}
impl Temp {
fn kelvin(self) -> f64 {
match self {
Temp::C(c) => c + 273.15,
Temp::F(f) => (f - 32.0) * 5.0 / 9.0 + 273.15,
Temp::K(k) => k,
}
}
}
impl PartialEq for Temp {
fn eq(&self, other: &Self) -> bool {
(self.kelvin() - other.kelvin()).abs() < 1e-9
}
}
impl PartialOrd for Temp {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.kelvin().partial_cmp(&other.kelvin())
}
}
impl fmt::Display for Temp {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Temp::C(v) => write!(f, "{v:.1}°C"),
Temp::F(v) => write!(f, "{v:.1}°F"),
Temp::K(v) => write!(f, "{v:.1}K"),
}
}
}
fn main() {
let readings = [Temp::C(21.0), Temp::F(75.0), Temp::K(290.0), Temp::F(69.8)];
let hottest = readings.iter().fold(readings[0], |a, &b| if b > a { b } else { a });
println!("hottest: {hottest}");
println!("{} == {}: {}", Temp::C(21.0), Temp::F(69.8), Temp::C(21.0) == Temp::F(69.8));
println!("{} < {}: {}", Temp::K(290.0), Temp::C(21.0), Temp::K(290.0) < Temp::C(21.0));
}hottest: 75.0°F
21.0°C == 69.8°F: true
290.0K < 21.0°C: trueSummary
- Associated types (
type Item;) let each implementation choose one type. Use them when there is one natural choice per type; use a generic trait (Trait<T>) when a type should implement it several times. - Supertraits (
trait A: B + C) require implementors to also implement other traits. - Operators are traits in
std::ops:Add,Sub,Mul,Neg,AddAssign,Index,IndexMut, and more.Summakes.sum()work. - When method names clash, call
Trait::method(&x), or<Type as Trait>::function()for associated functions. - The newtype pattern wraps a foreign type so you can implement foreign traits for it, and adds meaning and restrictions at zero cost.
- Marker traits label types; sealed traits can be used but not implemented outside their module.
Next: Conversion Traits.
