rust-trait-bounds-where-impl-trait

Trait bounds and impl Trait in Rust: useful constraints

  • 5 min

A trait bound is a restriction that requires a generic type to implement certain behaviors.

In the article on Generics we saw that we could define a function fn algo<T>(x: T). But if you tried to do println!("{}", x) inside that function, the compiler would give you an error.

Why? Because Rust is conservative. If you say that T can be any type, Rust assumes the worst-case scenario: it could be a type that doesn’t have a way to be printed (the Display trait).

To use methods or behaviors on a generic type, we need to constrain it. We need to tell the compiler: “I accept any type T, as long as it implements this Trait”.

This is called a Trait Bound.

Syntax summary

NeedSyntaxWhen to use it
Simplefn f(x: &impl Trait)Simple, distinct arguments.
Relationship between typesfn f<T: Trait>(x: &T, y: &T)When two arguments must be the same type.
Multiple TraitsT: Trait1 + Trait2When you need multiple capabilities.
Readabilitywhere T: TraitWhen the signature becomes too long.
Complex return types-> impl TraitIterators and Closures.

The simple way: impl Trait

We briefly saw this syntax in the previous article. It’s what we call “Syntactic Sugar”. It’s the most readable way to say “give me something that behaves like this”.

use std::fmt::Display;

// We accept "something displayable"
fn imprimir(valor: &impl Display) {
    println!("{}", valor);
}

fn main() {
    imprimir(&5);      // i32 implements Display
    imprimir("Hola");  // &str implements Display
}
Copied!

It’s great for simple cases. But it has a limitation: each parameter is independent.

If we write this:

fn comparar(a: &impl Resumible, b: &impl Resumible) { ... }
Copied!

We could pass a Tweet as the first argument and an Article as the second. Both are Resumible, but they are different types. What if we want to force them to be the same type?

The explicit way: trait bounds

For full control, we use the complete generic syntax: <T: Trait>. It reads: “For a type T that implements Trait…”.

// T must implement Display
fn imprimir<T: Display>(valor: &T) {
    println!("{}", valor);
}
Copied!

Require the same type

This syntax wins over impl Trait when we need to relate multiple types. If we need a function that compares two items, and we want to guarantee we aren’t mixing apples and oranges:

// a and b must be the SAME type T, and that T must be Resumible
fn comparar_misma_clase<T: Resumible>(a: &T, b: &T) {
    println!("Comparing {} with {}", a.resumir(), b.resumir());
}
Copied!

Combining constraints with +

What if we want a type that can be printed (Display) AND ALSO can be summarized (Resumible)?

We use the + operator.

fn notificar<T: Resumible + Display>(item: &T) {
    println!("Summary: {}", item.resumir());
    println!("Original: {}", item);
}
Copied!

This works both in the Trait Bound syntax and in the impl Trait syntax.

The where clause

When we start having complex functions with several generics and multiple constraints, the function signature becomes unreadable.

Look at this monster:

fn procesar<T: Display + Clone, U: Clone + Debug>(a: T, b: U) -> i32 {
    // ...
}
Copied!

It’s hard to see where the definition ends and the function body begins. To solve this, Rust allows us to move the constraints to the end of the signature using the where keyword.

fn procesar<T, U>(a: T, b: U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    // Now the code is clean and separated
    0
}
Copied!

It works exactly the same, it’s just a readability improvement. And in Rust, readability counts.

Returning impl Trait

So far, we’ve talked about input parameters. But what if we want to return a complex type without writing its very long name?

This is especially useful with iterators and closures. The types of adapters can be very long, and the concrete types of closures cannot be named directly.

We can say: “I’m going to return something that implements Iterator, don’t worry about the concrete type”.

fn crear_iterador() -> impl Iterator<Item = i32> {
    let v = vec![1, 2, 3];
    v.into_iter().map(|x| x * 2)
}
Copied!

The restriction of impl Trait in return: If you use -> impl Trait, the function must return a single concrete type. You cannot return a Tweet in an if and an Article in an else, even if both implement the Trait.

// ❌ This does NOT compile
fn noticia(es_corto: bool) -> impl Resumible {
    if es_corto {
        Tweet { ... }
    } else {
        Articulo { ... } // Error: different types
    }
}
Copied!

To return different concrete types under the same interface, we could use, for example, a trait object like Box<dyn Resumible>, accepting dynamic dispatch.