Nano Memories
19 jul 2025

Minimal Trait Bounds: Why and How?

The golden rule of any interface in the broad sense, and of traits in Rust in particular, is - don't require more than necessary

In this article, we'll cover:

Example: Iterator printer

Let's imagine we need to implement an iterator type that prints each of its elements. This type doesn't have much practical use, so treat it purely as an illustration

The type wraps an existing iterator, so we'll need a generic parameter for the inner iterator type - let's say something like I: Iterator. Since we want to print its elements, the iterator's items must implement the Display trait. As a result, the type could be declared like this:

use std::fmt::Display;

pub struct Printer<I>
where
    I: Iterator<Item: Display>,
{
    iter: I,
}

We don't want users accessing the iter field directly, so we'll make it private. Let's also implement the Iterator trait:

impl<I> Iterator for Printer<I>
where
    I: Iterator<Item: Display>,
{
    type Item = I::Item;

    fn next(&mut self) -> Option<Self::Item> {
        let item = self.iter.next()?;
        println!("{item}");
        Some(item)
    }
}

We'll add a constructor and a into_inner method to get the inner iterator back:

impl<I> Printer<I>
where
    I: Iterator<Item: Display>,
{
    pub fn new(iter: I) -> Self {
        Self { iter }
    }

    pub fn into_inner(self) -> I {
        self.iter
    }
}

Let's also implement a couple of useful traits since we're writing something library-like. For example, what if the user wants to clone a value of our type? Let's implement Clone - but I'll do it explicitly rather than using derive, purely for illustrative purposes. To be able to clone the contents, we'll require that it implements the Clone trait

impl<I> Clone for Printer<I>
where
    I: Iterator<Item: Display> + Clone,
{
    fn clone(&self) -> Self {
        Self {
            iter: self.iter.clone(),
        }
    }
}

What's the problem?

Well, at first glance everything seems logical, but even at this stage we can already see problems with this approach. Any impl that involves our type Printer<I> inevitably requires I: Iterator<Item: Display>. If we remove this bound, the code simply won't compile - even code that doesn't use Iterator at all:

impl<I> Printer<I> {
    //  ^^^^^^^^^^ `I` is not an iterator
    pub fn new(iter: I) -> Self {
        Self { iter }
    }

    pub fn into_inner(self) -> I {
        self.iter
    }
}

At the very least, this clutters the code with unnecessary details. Why should we require I: Iterator<Item: Display> just to construct, destructure, or clone a value of our type? Technically, there's nothing preventing us from constructing Printer<I> with any type. Especially in the implementation of the into_inner method - we just need to extract the value, nothing more. There's no need to iterate over it, and certainly no need to print its elements.

The code of our library becomes more complex for no good reason. Moreover, all these bounds will show up in the crate's documentation - even if the user doesn't look at the source code, they'll still see the trait bounds and think about them in places where they're not needed. That alone is a good enough reason to remove the bounds from all impls - except for the ones that truly need them. In particular: impl<I> Iterator for Printer<I>, because otherwise we simply wouldn't be able to implement the trait

Do you need bounds on type parameters?

Moreover, there's absolutely no need for any bounds in the declaration of a struct. When defining a struct or enum, we're describing only the data - unlike in functions or methods, there's no actual code involved. We can't really make use of the fact that a generic parameter implements Iterator, or any other trait, in the type definition itself. Only in rather exceptional cases does it make sense - a good example is the Peekable type from the standard library. Take note: it adds the I: Iterator bound at the type level, which means the bound applies to all its methods and trait implementations as well

Why is that needed? Let's open the source code and see exactly how Peekable is declared

pub struct Peekable<I>
where
    I: Iterator,
{
    iter: I,
    /// Remember a peeked value, even if it was None.
    peeked: Option<Option<I::Item>>,
}

Here, the purpose of the Peekable type becomes clear - it's an iterator that allows peeking one item ahead. For that to work, it needs to store an item somewhere. But what type will that item have? Naturally, it will be I::Item from the Iterator trait. But in order to refer to Item, we must require that I implements Iterator. Otherwise, we simply wouldn't know the associated type. In this case, the trait bound is completely justified

But what about our hypothetical Printer type? As we've already seen, applying bounds to the generic parameter is not necessary at all. Let's simplify the type definition accordingly

pub struct Printer<I> {
    iter: I,
}

Do you need bounds on impl blocks?

The entire point of our Printer type is to be an iterator. Without that constraint, the type doesn't make much sense - so maybe it's worth requiring I: Iterator<Item: Display> at least for clarity? Sure, it might clutter the library's code a bit, but it would make the intention behind the type immediately obvious. After all, if I, as the library author, write a bit more code, it might make it easier for the user to work with my types and traits. For example, what if a user creates a Printer value with something that's not an iterator inside? Wouldn't it be better for them to get an error right away rather than only when they try to use it as an iterator?

Indeed, there are cases where it's better to require the necessary trait implementation as early as possible. For instance, a user might be composing some complex iterator using many adapters, only to later find out the final type doesn't actually implement Iterator because one of its parts doesn't. Imagine writing code like this:

let n = (0..6).map("?").count();

It doesn't compile because in order to call the count method on the final type, self must be an iterator

But what's the issue here? If the map method didn't check the necessary bounds and simply constructed its adapter, then when calling count, I would have gotten the following error message:

error[E0599]: the method `count` exists for struct `Map<Range<{integer}>, &str>`, but its trait bounds were not satisfied
  --> src/main.rs:2:29
   |
2  |     let n = (0..6).map("?").count();
   |                             ^^^^^ method cannot be called on `Map<Range<{integer}>, &str>` due to unsatisfied trait bounds
   |
  ::: /home/nano/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/iter/adapters/map.rs:61:1
   |
61 | pub struct Map<I, F> {
   | -------------------- doesn't satisfy `Map<std::ops::Range<{integer}>, &str>: Iterator`
   |
   = note: the following trait bounds were not satisfied:
           `<&str as FnOnce<({integer},)>>::Output = _`
           which is required by `Map<std::ops::Range<{integer}>, &str>: Iterator`
           `&str: FnMut<({integer},)>`
           which is required by `Map<std::ops::Range<{integer}>, &str>: Iterator`
           `Map<std::ops::Range<{integer}>, &str>: Iterator`
           which is required by `&mut Map<std::ops::Range<{integer}>, &str>: Iterator`

Fortunately, this example is simple enough that I can still extract the root cause of the problem - namely, the line which is required by 'Map<std::ops::Range<{integer}>, &str>: Iterator' '&str: FnMut<({integer},)>', where I can see that &str doesn't implement the FnMut trait

But this is still quite hard to parse, and the more complex the iterator I'm building, the harder it gets. At some point, the error will become completely unreadable, and I won't be able to figure out what's actually wrong

Thankfully, the map method on iterators enforces the necessary trait bounds ahead of time, and before I hit the error above, I'll first see this:

error[E0277]: expected a `FnMut({integer})` closure, found `&str`
   --> src/main.rs:2:24
    |
2   |     let n = (0..6).map("?").count();
    |                    --- ^^^ expected an `FnMut({integer})` closure, found `&str`
    |                    |
    |                    required by a bound introduced by this call
    |
    = help: the trait `Fn({integer})` is not implemented for `str`
    = note: required for `&str` to implement `FnMut({integer})`
note: required by a bound in `map`
   --> /home/nano/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/iter/traits/iterator.rs:748:12
    |
745 |     fn map<B, F>(self, f: F) -> Map<Self, F>
    |        --- required by a bound in this associated function
...
748 |         F: FnMut(Self::Item) -> B,
    |            ^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Iterator::map`

Much clearer already: &str does not implement FnMut, and that's stated right at the beginning of the error log

The map method itself is implemented like this:

pub trait Iterator {
    /**/

    fn map<B, F>(self, f: F) -> Map<Self, F>
    where
        Self: Sized,
        F: FnMut(Self::Item) -> B,
    {
        Map::new(self, f)
    }

    /**/
}

It is clear here that the method signature requires F: FnMut(Self::Item) -> B for the function parameter. Although, in fact, a Map value can be constructed without this constraint. This example clearly shows in which cases it is useful to deviate from the minimal interface rule and require more, even when it is technically not necessary

Let's return to our example. To provide clearer error messages, it would make sense to leave the constraint I: Iterator<Item: Display> - but only on the constructor

The most optimal solution for our public API would be the following impl:

impl<I> Printer<I> {
    pub fn new(iter: I) -> Self
    where
        // The restriction is only here
        I: Iterator<Item: Display>,
    {
        Self { iter }
    }

    pub fn into_inner(self) -> I {
        self.iter
    }
}

Simplifying method signatures

This is a good compromise: we no longer require too much where it's unnecessary, but we still give the user an early warning that creating a value of this type only makes sense if I is an iterator - and not just any iterator, but one where Item: Display. At the same time, once the value is created, nothing prevents calling into_inner on it. This makes the user's code simpler in some places

For example, what if the user writes a function like this?

fn debug_inner<I>(printer: Printer<I>)
where
    I: Debug,
{
    println!("{:?}", printer.into_inner());
}

Here, the user's code requires exactly what is necessary - namely, Debug for the inner value and nothing more. In the earlier version of the library with unnecessary constraints, they would have to write something like this:

fn debug_inner<I>(printer: Printer<I>)
where
    I: Iterator<Item: Display> + Debug,
    // ^^^^^^^^^^^^^^^^^^^^^^^ this constraint is not necessary for the implementation
{
    println!("{:?}", printer.into_inner());
}

Even though neither iteration nor printing of the iterator elements is used in any way here. The extra constraints only add confusion, making the code not just more complex, but also harder to understand. A user, upon seeing such a signature, might naturally assume that the function will perform iteration and printing of elements - but that's not the case

Another use case: what if no methods of the type are called at all? What if the user simply wants to store values of our type inside another structure?

use std::collections::HashMap;

fn check_key_hello<I>(m: &HashMap<&str, Printer<I>>) -> bool {
    m.contains_key("hello")
}

If the constraints weren't minimal and were instead applied to the entire struct, this would also require specifying I: Iterator<Item: Display>, even though that makes no sense here. Again, HashMap is just an example - there could be a custom user type that contains Printer<I> as a field, or anything else. In any case, there's a lot of potential code that doesn't use types for their primary purpose but instead works with them indirectly

Now let's look at some user code like this:

fn make_boxed_printer<I>(iter: I) -> Box<Printer<I>>
where
    I: Iterator<Item: Display>,
{
    Box::new(Printer::new(iter))
}

Is I: Iterator<Item: Display> needed here? Technically, no - it's not required for the code to compile. However, earlier we decided that requiring the Iterator implementation in the new constructor might be helpful

Simplifying trait and type signatures

I hope I managed to demonstrate the questionable nature of such code:

struct Foo<T>(Vec<T>)
where
    T: Display;

And especially, if you see something like this:

struct Foo<T>(Vec<T>)
where
    T: Clone + Display + Debug + PartialEq + Eq + Send + Sync;

This clearly indicates that something is off with the code and that it can be significantly simplified. Such constraints are never necessary in practice - they only serve to clutter the code

Similarly, if you come across something like this:

trait Foo: Clone + Display + Debug + PartialEq + Eq + Send + Sync {/**/}

It's also worth considering whether all these constraints are truly necessary, even if they are "required" and ultimately used in the implementations. A more robust approach is to start not from the implementations, but from the actual requirements of the trait - that is, how the trait is intended to be used. More specific bounds make sense only in a specific context, rather than in the trait definition itself:

trait Foo {/**/}

fn use_comparable_foo<F>(f: F)
where
    F: Foo + Eq,
{/**/}

It's also worth noting that many existing traits already come with constraints. For example, the Ord trait from the standard library requires Eq and PartialOrd as prerequisites. Moreover, Eq itself requires PartialEq

pub trait Ord: Eq + PartialOrd {/**/}
pub trait Eq: PartialEq {}

Therefore, there's no practical reason to write a signature like this:

fn use_t<T>(t: T)
where
    T: PartialEq + Eq + PartialOrd + Ord,
{/**/}

Whereas in reality, only Ord is strictly required.

It also makes sense to split constraints more granularly:

impl<T> Foo<T>
where
    T: Clone + Display + Debug + PartialEq + Eq + Send + Sync,
{
    fn foo(&self) {/**/}
    fn bar(&self) {/**/}
}

Whenever possible, localize trait bounds and, instead of this, try to express the requirements more precisely.

impl<T> Foo<T> {
    fn foo(&self)
    where
        T: Clone + Display,
    {/**/}

    fn bar(&self)
    where
        T: Eq,
    {/**/}
}

Conclusion

Still, there are no hard and fast rules for every situation. However, carefully considering how your public API looks is important. This is especially relevant for libraries, though issues like code bloat and complexity can arise even in more localized code. In general, I prefer to keep the code as minimal as possible and the interfaces concise. Also, when changing an implementation, consider revisiting the trait bounds - some of them might no longer be necessary. Try removing a bound and see whether it still compiles

I firmly believe that being mindful of trait bounds is a good practice. I also hope to explore this topic further using a more common real-world example - everyone's favorite: Send + Sync + 'static. But that will be the subject of another article

To conclude this one - why go through all of this? Here's what we gain:

Bonus: Trait aliases

But what if we do need to specify multiple trait bounds - across many places? For example, suppose we're working with the Read and Write traits, and we need to repeatedly write I: Read + Write. Or maybe there are even more traits involved?

In such cases, we can use what's informally known as a "trait alias". Rust doesn't currently support true trait aliases natively, so we use a slightly more verbose pattern - an idiom commonly referred to as a "blanket implementation":

pub trait Io: Read + Write {}
impl<I> Io for I where I: Read + Write {}

Great, now the bound I: Io is fully interchangeable with I: Read + Write. It works both ways: any type that implements Read + Write can be used as Io and vice versa.

Sometimes, this can genuinely simplify the code. However, this pattern should be used with caution. It's very easy to abuse, to the point where users may struggle to understand what constraints are actually required for their types. It also introduces an extra layer of mental abstraction with no functional gain - after all, it creates a new trait that needs to be described, documented, and integrated into the existing API

So, try to use this pattern only when it's truly justified.