Guide contents

Lesson 32 of 94

Trait Bounds & where Clauses

The previous lesson introduced bounds like T: PartialOrd. This lesson covers everything you can do with them: combining several bounds, moving them into where clauses, putting them on impl blocks so methods exist only for some types, and implementing a trait for every type that meets a bound.

Use case Rust
Requiring an ordering <T: Ord>
Combining several bounds <T: Copy + Ord> or a where clause
A slice of any Shape &[impl Shape] or &[T] where T: Shape
Methods that exist only for some T impl<T: Display> Wrapper<T> { ... }
Implementing a trait for every matching type Blanket impls: impl<T: Display> MyTrait for T

1. Several bounds with +

Rust
use std::fmt::Display;

fn announce_winner<T: Display + PartialOrd>(a: T, b: T) {
    if a >= b {
        println!("{a} wins over {b}");
    } else {
        println!("{b} wins over {a}");
    }
}

fn main() {
    announce_winner(42, 17);
    announce_winner("zebra", "apple");
    announce_winner(2.5, 9.75);
}
Output
42 wins over 17
zebra wins over apple
9.75 wins over 2.5

Display is needed to print with {}, and PartialOrd to compare with >=. Each bound unlocks exactly the operations you use.

2. Printing a generic value needs Debug or Display

Rust✗ does not compile · E0277
fn show<T>(value: T) {
    println!("{:?}", value);
}

fn main() {
    show(5);
}
Output
error[E0277]: `T` doesn't implement `Debug`
 --> src/main.rs:2:22
  |
2 |     println!("{:?}", value);
  |               ----   ^^^^^ `T` cannot be formatted using `{:?}` because it doesn't implement `Debug`
  |               |
  |               required by this formatting parameter
  |
help: consider restricting type parameter `T` with trait `Debug`
  |
1 | fn show<T: std::fmt::Debug>(value: T) {
  |          +++++++++++++++++

The fix is fn show<T: std::fmt::Debug>(value: T). The compiler's help: suggests exactly that.

3. where clauses

When bounds get long, move them after the signature into a where clause. It means exactly the same thing and reads better.

Rust
use std::fmt::{Debug, Display};

fn report<K, V>(key: K, values: &[V]) -> String
where
    K: Display,
    V: Debug + Clone + PartialOrd,
{
    let mut sorted = values.to_vec();
    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
    format!("{key}: {:?}", sorted)
}

fn main() {
    println!("{}", report("latencies", &[120, 45, 300]));
    println!("{}", report(2026, &["q3", "q1", "q2"]));
}
Output
latencies: [45, 120, 300]
2026: ["q1", "q2", "q3"]

4. impl Trait parameters vs generic parameters

fn f(x: impl Trait) is shorthand for fn f<T: Trait>(x: T). The difference shows up with two parameters: two impl Trait parameters may be different types, while one type parameter T forces them to be the same type.

Rust
use std::fmt::Display;

fn pair_any(a: impl Display, b: impl Display) -> String {
    format!("{a} & {b}")
}

fn pair_same<T: Display>(a: T, b: T) -> String {
    format!("{a} & {b}")
}

fn main() {
    println!("{}", pair_any(1, "one"));     // i32 and &str: fine
    println!("{}", pair_same("x", "y"));    // both &str: fine
    println!("{}", pair_same(1.5, 2.5));    // both f64: fine
}
Output
1 & one
x & y
1.5 & 2.5
Rust✗ does not compile · E0308
use std::fmt::Display;

fn pair_same<T: Display>(a: T, b: T) -> String {
    format!("{a} & {b}")
}

fn main() {
    println!("{}", pair_same(1, "one"));
}
Output
error[E0308]: mismatched types
 --> src/main.rs:8:33
  |
8 |     println!("{}", pair_same(1, "one"));
  |                    --------- -  ^^^^^ expected integer, found `&str`
  |                    |         |
  |                    |         expected all arguments to be this integer type because they need to match the type of this parameter
  |                    arguments to this function are incorrect
  |
note: function defined here
 --> src/main.rs:3:4
  |
3 | fn pair_same<T: Display>(a: T, b: T) -> String {
  |    ^^^^^^^^^ -           ----  ---- this parameter needs to match the integer type of `a`
  |              |           |
  |              |           `b` needs to match the integer type of this parameter
  |              `a` and `b` both reference this parameter `T`

5. Bounds on a whole impl block: conditional methods

Putting a bound on the impl block makes its methods exist only for types that meet the bound. Other methods of the same type remain available for every T.

Rust
use std::fmt::Display;

struct Pair<T> {
    a: T,
    b: T,
}

impl<T> Pair<T> {
    fn new(a: T, b: T) -> Self {
        Self { a, b }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn show_larger(&self) {
        if self.a >= self.b {
            println!("larger: {}", self.a);
        } else {
            println!("larger: {}", self.b);
        }
    }
}

struct Opaque;

fn main() {
    Pair::new(3, 8).show_larger();
    Pair::new("kiwi", "fig").show_larger();
    let _p = Pair::new(Opaque, Opaque); // allowed: `new` has no bound
    // _p.show_larger();                 // does not compile: Opaque is not Display + PartialOrd
}
Output
larger: 8
larger: kiwi

6. Calling a conditional method on the wrong type

Rust✗ does not compile · E0599
use std::fmt::Display;

struct Pair<T> {
    a: T,
    b: T,
}

impl<T: Display + PartialOrd> Pair<T> {
    fn show_larger(&self) {
        println!("{}", if self.a >= self.b { &self.a } else { &self.b });
    }
}

struct Opaque;

fn main() {
    let p = Pair { a: Opaque, b: Opaque };
    p.show_larger();
}
Output
error[E0599]: the method `show_larger` exists for struct `Pair<Opaque>`, but its trait bounds were not satisfied
  --> src/main.rs:18:7
   |
 3 | struct Pair<T> {
   | -------------- method `show_larger` not found for this struct
...
14 | struct Opaque;
   | ------------- doesn't satisfy `Opaque: PartialOrd` or `Opaque: std::fmt::Display`
...
18 |     p.show_larger();
   |       ^^^^^^^^^^^ method cannot be called on `Pair<Opaque>` due to unsatisfied trait bounds
   |
note: the following trait bounds were not satisfied:
      `Opaque: PartialOrd`
      `Opaque: std::fmt::Display`
  --> src/main.rs:8:9
   |
 8 | impl<T: Display + PartialOrd> Pair<T> {
   |         ^^^^^^^   ^^^^^^^^^^  -------
   |         |         |
   |         |         unsatisfied trait bound introduced here
   |         unsatisfied trait bound introduced here
note: the trait `std::fmt::Display` must be implemented
  --> /rustc/<version>/library/core/src/fmt/mod.rs:1186:0
help: consider annotating `Opaque` with `#[derive(PartialEq, PartialOrd)]`
   |
14 + #[derive(PartialEq, PartialOrd)]
15 | struct Opaque;
   |

7. Blanket implementations

You can implement a trait for every type that satisfies a bound. The standard library does this everywhere; for example, every type that implements Display automatically gets ToString.

Rust
use std::fmt::Display;

trait Loud {
    fn loud(&self) -> String;
}

impl<T: Display> Loud for T {
    fn loud(&self) -> String {
        format!("{}!!!", self.to_string().to_uppercase())
    }
}

fn main() {
    println!("{}", "hello".loud());
    println!("{}", 42.loud());
    println!("{}", 'x'.loud());
    println!("{}", 3.5.loud());
}
Output
HELLO!!!
42!!!
X!!!
3.5!!!

One impl added the method to &str, i32, char, f64, and every other printable type, including types defined later by other code.

8. A generic function over your own trait

Rust
trait Area {
    fn area(&self) -> f64;
}

struct Square(f64);
struct Circle(f64);

impl Area for Square {
    fn area(&self) -> f64 {
        self.0 * self.0
    }
}

impl Area for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.0 * self.0
    }
}

fn total_area<T: Area>(shapes: &[T]) -> f64 {
    let mut total = 0.0;
    for s in shapes {
        total += s.area();
    }
    total
}

fn main() {
    let tiles = [Square(1.0), Square(2.0), Square(0.5)];
    let coins = [Circle(1.0), Circle(1.0)];
    println!("{:.2}", total_area(&tiles));
    println!("{:.2}", total_area(&coins));
}
Output
5.25
6.28

A slice &[T] holds values of one type T, so total_area can't take squares and circles mixed together. Mixed collections need trait objects, the next lesson.

9. Generic arithmetic with std::ops

Operators are traits too: + is std::ops::Add, * is Mul, and so on. A generic sum needs to say that T can be added and has a zero value.

Rust
use std::ops::Add;

fn sum<T>(values: &[T]) -> T
where
    T: Copy + Default + Add<Output = T>,
{
    let mut total = T::default();
    for &v in values {
        total = total + v;
    }
    total
}

fn main() {
    println!("{}", sum(&[1, 2, 3, 4]));
    println!("{}", sum(&[0.5, 0.25, 0.125]));
    println!("{}", sum::<u8>(&[]));
}
Output
10
0.875
0

Add<Output = T> means "adding two Ts gives a T". Output is an associated type, explained in Advanced Traits.

10. Generic average with conversion

To divide by the count, the values need converting to f64. The bound Into<f64> accepts every type that converts to f64 losslessly.

Rust
fn average<T>(values: &[T]) -> Option<f64>
where
    T: Copy + Into<f64>,
{
    if values.is_empty() {
        return None;
    }
    let mut total = 0.0;
    for &v in values {
        total += v.into();
    }
    Some(total / values.len() as f64)
}

fn main() {
    println!("{:?}", average(&[1_u8, 2, 4]));
    println!("{:?}", average(&[10_i32, -4, 3]));
    println!("{:?}", average(&[0.1_f32, 0.2]));
    println!("{:?}", average::<u16>(&[]));
}
Output
Some(2.3333333333333335)
Some(3.0)
Some(0.15000000223517418)
None

i64 does not implement Into<f64>, because large i64 values can't be represented exactly. The compiler would reject average(&[1_i64]), which is exactly the kind of silent precision loss the trait bound prevents.

11. Bounds that relate two type parameters

A bound can mention another type parameter. T: PartialEq<U> means "a T can be compared with a U".

Rust
fn count_matches<T, U>(items: &[T], target: &U) -> usize
where
    T: PartialEq<U>,
{
    let mut n = 0;
    for item in items {
        if item == target {
            n += 1;
        }
    }
    n
}

fn main() {
    let names = vec![String::from("ana"), String::from("ben"), String::from("ana")];
    println!("{}", count_matches(&names, &"ana")); // String compared with &str
    println!("{}", count_matches(&[1, 2, 1, 1], &1));
}
Output
2
3

12. Returning impl Trait

A function can return "some type implementing a trait" without naming it. The caller can only use the trait's methods.

Rust
use std::fmt::Display;

fn status_label(code: u16) -> impl Display {
    format!("HTTP {code}")
}

fn evens(limit: u32) -> impl Iterator<Item = u32> {
    (0..limit).filter(|n| n % 2 == 0)
}

fn main() {
    println!("{}", status_label(404));
    let v: Vec<u32> = evens(10).collect();
    println!("{:?}", v);
}
Output
HTTP 404
[0, 2, 4, 6, 8]

Returning iterators as impl Iterator<Item = T> is the most common use. Their real types are often long and unnameable.

13. impl Trait returns must be one type

Rust✗ does not compile · E0308
trait Shape {
    fn area(&self) -> f64;
}

struct Square(f64);
struct Circle(f64);

impl Shape for Square {
    fn area(&self) -> f64 { self.0 * self.0 }
}

impl Shape for Circle {
    fn area(&self) -> f64 { 3.14 * self.0 * self.0 }
}

fn make(round: bool) -> impl Shape {
    if round { Circle(1.0) } else { Square(1.0) }
}

fn main() {
    println!("{}", make(true).area());
}
Output
error[E0308]: `if` and `else` have incompatible types
  --> src/main.rs:17:37
   |
17 |     if round { Circle(1.0) } else { Square(1.0) }
   |                -----------          ^^^^^^^^^^^ expected `Circle`, found `Square`
   |                |
   |                expected because of this
   |
help: you could change the return type to be a boxed trait object
   |
16 - fn make(round: bool) -> impl Shape {
16 + fn make(round: bool) -> Box<dyn Shape> {
   |
help: if you change the return type to expect trait objects, box the returned expressions
   |
17 |     if round { Box::new(Circle(1.0)) } else { Box::new(Square(1.0)) }
   |                +++++++++           +          +++++++++           +

The fix is Box<dyn Shape>, a trait object, in the next lesson.

14. ?Sized: accepting str and slices directly

Type parameters normally must have a known size at compile time. str and [T] don't, which is why they are always used behind references. Adding ?Sized relaxes that requirement, so T can be str itself.

Rust
use std::fmt::Display;

fn bracket<T: Display + ?Sized>(value: &T) -> String {
    format!("[{value}]")
}

fn main() {
    let owned = String::from("owned");
    println!("{}", bracket("a str literal")); // T = str
    println!("{}", bracket(&owned));          // T = String
    println!("{}", bracket(&42));             // T = i32
}
Output
[a str literal]
[owned]
[42]

Without ?Sized, calling bracket("a str literal") would infer T = str and fail with "the size for values of type str cannot be known".

15. Generic methods in a trait

A trait method can be generic itself.

Rust
use std::fmt::Debug;

trait Logger {
    fn log<T: Debug>(&self, label: &str, value: &T);
}

struct Console {
    prefix: &'static str,
}

impl Logger for Console {
    fn log<T: Debug>(&self, label: &str, value: &T) {
        println!("{} {label} = {:?}", self.prefix, value);
    }
}

fn main() {
    let log = Console { prefix: "[app]" };
    log.log("retries", &3);
    log.log("hosts", &vec!["a", "b"]);
    log.log("ratio", &Some(0.75));
}
Output
[app] retries = 3
[app] hosts = ["a", "b"]
[app] ratio = Some(0.75)

Traits with generic methods can't be used as dyn Trait. The next lesson explains why.

16. Bounds on impl for a trait: implementing a trait conditionally

You can implement a trait for a generic type only when its parameter meets a bound. Here Wrapper<T> is Display only when T is.

Rust
use std::fmt;

struct Labeled<T> {
    label: &'static str,
    value: T,
}

impl<T: fmt::Display> fmt::Display for Labeled<T> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}: {}", self.label, self.value)
    }
}

fn main() {
    println!("{}", Labeled { label: "cpu", value: "87%" });
    println!("{}", Labeled { label: "uptime_days", value: 42 });
    let hidden = Labeled { label: "raw", value: vec![1, 2] };
    // println!("{hidden}"); // does not compile: Vec<i32> is not Display
    println!("{} holds {} numbers", hidden.label, hidden.value.len());
}
Output
cpu: 87%
uptime_days: 42
raw holds 2 numbers

This is exactly how Vec<T> is Debug only when T: Debug, and Clone only when T: Clone.

17. Static dispatch has no runtime cost

Generic calls are resolved at compile time. Each call site calls the specialized function directly, and the optimizer can inline it.

Rust
trait Discount {
    fn apply(&self, cents: u64) -> u64;
}

struct Percent(u64);
struct Flat(u64);

impl Discount for Percent {
    fn apply(&self, cents: u64) -> u64 {
        cents - cents * self.0 / 100
    }
}

impl Discount for Flat {
    fn apply(&self, cents: u64) -> u64 {
        cents.saturating_sub(self.0)
    }
}

fn checkout<D: Discount>(items: &[u64], discount: &D) -> u64 {
    let mut total = 0;
    for &i in items {
        total += discount.apply(i); // a direct call to Percent::apply or Flat::apply
    }
    total
}

fn main() {
    let cart = [1_000, 2_500, 4_000];
    println!("{}", checkout(&cart, &Percent(10)));
    println!("{}", checkout(&cart, &Flat(300)));
}
Output
6750
6600

18. Realistic example: top N of anything

Rust
fn top_n<T: Ord + Clone>(items: &[T], n: usize) -> Vec<T> {
    let mut sorted = items.to_vec();
    sorted.sort();
    sorted.reverse();
    sorted.truncate(n);
    sorted
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Score {
    points: u32,
    player: String,
}

fn main() {
    println!("{:?}", top_n(&[4, 9, 1, 7, 9, 3], 3));
    println!("{:?}", top_n(&["pear", "apple", "zucchini", "fig"], 2));
    let scores = vec![
        Score { points: 120, player: String::from("ana") },
        Score { points: 340, player: String::from("ben") },
        Score { points: 200, player: String::from("cy") },
    ];
    for s in top_n(&scores, 2) {
        println!("{} {}", s.player, s.points);
    }
}
Output
[9, 9, 7]
["zucchini", "pear"]
ben 340
cy 200

19. Realistic example: counting occurrences of any hashable value

HashMap keys must implement Hash + Eq. HashMap gets its own lesson in Collections; here it just counts.

Rust
use std::collections::HashMap;
use std::hash::Hash;

fn frequencies<T>(items: &[T]) -> Vec<(T, usize)>
where
    T: Hash + Eq + Clone + Ord,
{
    let mut counts: HashMap<T, usize> = HashMap::new();
    for item in items {
        *counts.entry(item.clone()).or_insert(0) += 1;
    }
    let mut result: Vec<(T, usize)> = counts.into_iter().collect();
    result.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
    result
}

fn main() {
    let status_codes = [200, 404, 200, 500, 200, 404];
    println!("{:?}", frequencies(&status_codes));
    let words = ["rust", "java", "rust", "go", "java", "rust"];
    println!("{:?}", frequencies(&words));
}
Output
[(200, 3), (404, 2), (500, 1)]
[("rust", 3), ("java", 2), ("go", 1)]

20. Realistic example: a generic repository

A repository that works for any entity type, as long as the entity can report its ID. The requirement is expressed with a trait bound.

Rust
trait Entity {
    fn id(&self) -> u64;
}

#[derive(Debug, Clone)]
struct User {
    id: u64,
    email: String,
}

#[derive(Debug, Clone)]
struct Product {
    id: u64,
    name: String,
    price_cents: u32,
}

impl Entity for User {
    fn id(&self) -> u64 {
        self.id
    }
}

impl Entity for Product {
    fn id(&self) -> u64 {
        self.id
    }
}

struct Repository<T: Entity> {
    rows: Vec<T>,
}

impl<T: Entity + Clone> Repository<T> {
    fn new() -> Self {
        Self { rows: Vec::new() }
    }

    fn save(&mut self, entity: T) {
        match self.rows.iter_mut().find(|r| r.id() == entity.id()) {
            Some(existing) => *existing = entity,
            None => self.rows.push(entity),
        }
    }

    fn find(&self, id: u64) -> Option<&T> {
        self.rows.iter().find(|r| r.id() == id)
    }

    fn count(&self) -> usize {
        self.rows.len()
    }
}

fn main() {
    let mut users = Repository::new();
    users.save(User { id: 1, email: String::from("ana@example.com") });
    users.save(User { id: 2, email: String::from("ben@example.com") });
    users.save(User { id: 1, email: String::from("ana@new.example.com") });
    println!("{} users, #1 = {:?}", users.count(), users.find(1));

    let mut products = Repository::new();
    products.save(Product { id: 10, name: String::from("lamp"), price_cents: 2_999 });
    println!("{:?}", products.find(10).map(|p| (&p.name, p.price_cents)));
    println!("{:?}", products.find(99).is_none());
}
Output
2 users, #1 = Some(User { id: 1, email: "ana@new.example.com" })
Some(("lamp", 2999))
true

21. Realistic example: a table printer for any rows

Rust
use std::fmt::Display;

trait Row {
    fn cells(&self) -> Vec<String>;
}

impl<A: Display, B: Display, C: Display> Row for (A, B, C) {
    fn cells(&self) -> Vec<String> {
        vec![self.0.to_string(), self.1.to_string(), self.2.to_string()]
    }
}

fn print_table<R: Row>(headers: &[&str], rows: &[R]) {
    let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
    let rendered: Vec<Vec<String>> = rows.iter().map(|r| r.cells()).collect();
    for row in &rendered {
        for (i, cell) in row.iter().enumerate() {
            widths[i] = widths[i].max(cell.len());
        }
    }
    let line = |cells: &[String]| {
        let parts: Vec<String> = cells.iter().enumerate().map(|(i, c)| format!("{:<w$}", c, w = widths[i])).collect();
        println!("| {} |", parts.join(" | "));
    };
    line(&headers.iter().map(|h| h.to_string()).collect::<Vec<_>>());
    for row in &rendered {
        line(row);
    }
}

fn main() {
    print_table(&["service", "status", "p99 ms"], &[("auth", "ok", 42), ("payments", "degraded", 1_870), ("search", "ok", 95)]);
    println!();
    print_table(&["x", "y", "label"], &[(1.5, -2, 'A'), (10.25, 7, 'B')]);
}
Output
| service  | status   | p99 ms |
| auth     | ok       | 42     |
| payments | degraded | 1870   |
| search   | ok       | 95     |

| x     | y  | label |
| 1.5   | -2 | A     |
| 10.25 | 7  | B     |

The blanket-style impl<A: Display, B: Display, C: Display> Row for (A, B, C) makes every three-element tuple of printable values a Row.

22. Realistic example: validators generic over their input

A trait can itself be generic, so one validator trait works for inputs of any type.

Rust
trait Validator<T: ?Sized> {
    fn validate(&self, value: &T) -> Result<(), String>;
}

struct Range {
    min: i64,
    max: i64,
}

struct MaxLen(usize);

impl Validator<i64> for Range {
    fn validate(&self, v: &i64) -> Result<(), String> {
        if (self.min..=self.max).contains(v) { Ok(()) } else { Err(format!("{v} not in {}..={}", self.min, self.max)) }
    }
}

impl Validator<str> for MaxLen {
    fn validate(&self, v: &str) -> Result<(), String> {
        if v.chars().count() <= self.0 { Ok(()) } else { Err(format!("'{v}' longer than {}", self.0)) }
    }
}

fn check<T: ?Sized, V: Validator<T>>(label: &str, value: &T, validator: &V) {
    match validator.validate(value) {
        Ok(()) => println!("{label}: ok"),
        Err(e) => println!("{label}: {e}"),
    }
}

fn main() {
    check("age", &34, &Range { min: 13, max: 120 });
    check("age", &7, &Range { min: 13, max: 120 });
    check("username", "ferris", &MaxLen(12));
    check("username", "a_very_long_username", &MaxLen(12));
}
Output
age: ok
age: 7 not in 13..=120
username: ok
username: 'a_very_long_username' longer than 12

23. Realistic example: merging two sorted lists of anything

Rust
fn merge_sorted<T: Ord + Clone>(a: &[T], b: &[T]) -> Vec<T> {
    let mut out = Vec::with_capacity(a.len() + b.len());
    let (mut i, mut j) = (0, 0);
    while i < a.len() && j < b.len() {
        if a[i] <= b[j] {
            out.push(a[i].clone());
            i += 1;
        } else {
            out.push(b[j].clone());
            j += 1;
        }
    }
    out.extend_from_slice(&a[i..]);
    out.extend_from_slice(&b[j..]);
    out
}

fn main() {
    println!("{:?}", merge_sorted(&[1, 4, 9], &[2, 3, 10, 11]));
    println!("{:?}", merge_sorted(&["ant", "cat"], &["bee", "dog", "eel"]));
}
Output
[1, 2, 3, 4, 9, 10, 11]
["ant", "bee", "cat", "dog", "eel"]

24. Realistic example: a builder that accepts anything string-like

impl Into<String> parameters let callers pass &str, String, or anything else convertible, and the function stores an owned String. It's a very common pattern in Rust APIs; Conversion Traits covers Into fully.

Rust
#[derive(Debug, Default)]
struct Email {
    to: Vec<String>,
    subject: String,
    body: String,
}

impl Email {
    fn to(mut self, address: impl Into<String>) -> Self {
        self.to.push(address.into());
        self
    }

    fn subject(mut self, s: impl Into<String>) -> Self {
        self.subject = s.into();
        self
    }

    fn body(mut self, b: impl Into<String>) -> Self {
        self.body = b.into();
        self
    }
}

fn main() {
    let owner = String::from("owner@example.com");
    let ticket = 4812;
    let email = Email::default()
        .to("ops@example.com")        // &str
        .to(owner)                    // String, moved in without a copy
        .subject(format!("Ticket #{ticket} escalated"))
        .body("Please take a look.");
    println!("{:#?}", email);
}
Output
Email {
    to: [
        "ops@example.com",
        "owner@example.com",
    ],
    subject: "Ticket #4812 escalated",
    body: "Please take a look.",
}

25. Realistic example: a generic event bus with where clauses

Rust
use std::fmt::Debug;

trait Event: Debug {
    fn topic(&self) -> &'static str;
}

#[derive(Debug)]
struct Signup {
    email: String,
}

#[derive(Debug)]
struct Purchase {
    cents: u64,
}

impl Event for Signup {
    fn topic(&self) -> &'static str {
        "users"
    }
}

impl Event for Purchase {
    fn topic(&self) -> &'static str {
        "billing"
    }
}

struct Bus {
    log: Vec<String>,
}

impl Bus {
    fn publish<E>(&mut self, event: E)
    where
        E: Event,
    {
        self.log.push(format!("[{}] {:?}", event.topic(), event));
    }

    fn publish_all<E, I>(&mut self, events: I)
    where
        E: Event,
        I: IntoIterator<Item = E>,
    {
        for e in events {
            self.publish(e);
        }
    }
}

fn main() {
    let mut bus = Bus { log: Vec::new() };
    bus.publish(Signup { email: String::from("ana@example.com") });
    bus.publish_all(vec![Purchase { cents: 1_999 }, Purchase { cents: 450 }]);
    bus.publish_all([Signup { email: String::from("ben@example.com") }]);
    for line in &bus.log {
        println!("{line}");
    }
}
Output
[users] Signup { email: "ana@example.com" }
[billing] Purchase { cents: 1999 }
[billing] Purchase { cents: 450 }
[users] Signup { email: "ben@example.com" }

trait Event: Debug means every Event must also be Debug, a supertrait, covered in Advanced Traits. I: IntoIterator<Item = E> accepts a Vec, an array, or any other iterable of events.

26. Realistic example: a generic clamp for configuration values

Rust
use std::fmt::Display;

fn clamp_setting<T>(name: &str, value: T, min: T, max: T) -> T
where
    T: PartialOrd + Display + Copy,
{
    if value < min {
        println!("{name}={value} is below {min}, using {min}");
        min
    } else if value > max {
        println!("{name}={value} is above {max}, using {max}");
        max
    } else {
        value
    }
}

fn main() {
    let workers = clamp_setting("workers", 64_u32, 1, 32);
    let ratio = clamp_setting("sample_ratio", 0.05_f64, 0.1, 1.0);
    let port = clamp_setting("port", 8080_u16, 1024, 65535);
    println!("workers={workers} ratio={ratio} port={port}");
}
Output
workers=64 is above 32, using 32
sample_ratio=0.05 is below 0.1, using 0.1
workers=32 ratio=0.1 port=8080

Summary

  • Combine bounds with +: T: Display + PartialOrd. Move long bounds into a where clause.
  • impl Trait in a parameter is shorthand for a generic parameter. Two impl Trait parameters can be different types; one T can't.
  • Bounds on an impl block make methods conditional. Bounds on a trait impl make a type implement a trait conditionally.
  • Blanket implementations (impl<T: Display> MyTrait for T) add a trait to every type meeting a bound.
  • Operators are traits in std::ops, so generic arithmetic uses bounds like Add<Output = T>.
  • ?Sized lets a type parameter be str or [T].
  • -> impl Trait returns one unnamed concrete type. Returning different types needs Box<dyn Trait>.

Next: Trait Objects & Dynamic Dispatch.