Purpose of where clause when implementing trait for concrete type

What is the purpose of a where clause in a trait implementation for a concrete type?

While re-reading the book “Rust for Rustaceans” by @jonhoo, I stumbled across listing 2-6 that demonstrates “an excessively generic implementation of Debug for any iterable collection”. As given in the book, the listing reads

impl Debug for AnyIterable
  where for<'a> &'a Self: IntoIterator,
        for<'a> <&'a Self as IntoIterator>::Item: Debug {
    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
        f.debug_list().entries(self).finish()
}}

The book says:

You could copy-paste this implementation for pretty much any collection type and it would “just work.” Of course, you may want a smarter debug implementation, but this illustrates the power of trait bounds quite well.

I believed to have understood that trait bounds are used in generic programming to specify the required capacities of a generic type. But in the above example everything is concrete - how would this serve as a demonstration of the power of trait bounds?

Here is a complete program that includes the above snippet, applied to SomeIterable. And, sure enough, it compiles with the where clause commented out:

struct SomeIterable(Vec<String>);

impl<'a> IntoIterator for &'a SomeIterable {
    type Item = &'a String;
    type IntoIter = std::slice::Iter<'a, String>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.as_slice().into_iter()
    }
}

use std::fmt::{Debug, Error, Formatter};

impl Debug for SomeIterable
// where
//     for<'a> &'a Self: IntoIterator,
//     for<'a> <&'a Self as IntoIterator>::Item: Debug,
{
    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
        f.debug_list().entries(self).finish()
    }
}

fn main() {
    let v = vec![String::from("hello"), String::from("world")];
    let v = SomeIterable(v);
    dbg!(&v);
}

I must be missing something obvious here, so I hesitated to bother this friendly forum. But unfortunately, I was not able find any discussion of this topic – neither here, nor elsewhere on the web, nor in books that I have.

Is the purpose only to serve as a form of “static typing for a template”, i.e. to provide clearer error messages when the snippet is used for some type in the wild that does not qualify?

1 Like

The concept you're looking for is called "trait bounds", and it serves as a way to bound or limit your traits.

For example:

trait MyTrait: Debug

Is read as a" trait named MyTrait, with the trait Debug as its bound". And it means that whatever type that wants to implement MyTrait, must also implement the Debug trait.

You can have bounds on different parts of a trait declaration as well, such as associated types and generic parameters.

I hope to have made it clear that I understand quite well what trait bounds are. My question is very specific: why does the author of the book consider unnecessary trait bounds an illustration of their power in this particular case?

And more generally: What is the purpose of a where clause when implementing a trait for a concrete type? (i.e. the subject of this thread...)

They understand that. Their (quite valid) question is: why put trait bounds on a concrete impl, where those trait bounds are either trivially true or false depending on the named type?

I don't have the book mentioned, but I agree it seems odd. It's possible that they were used simply as an example of how trait bounds might be used, and you're supposed to ignore the lack of generics... but that would be very confusing.

Sorry, didn't read that part. I guess Jon was complaining about an implementation being too generic since in Rust there's no specialization yet, so this implementation would prevent you from having more specialized implementations.

(Based on the immediately preceding material,) I think the example was just trying to illustrate associated type bounds and higher-ranked trait bounds -- namely a bound on references to the implementing type, which required an explicit for<'a> &'a Self: ... (in contrast with F: Fn(&T) -> &U where you can elide the for<..>). And I think it was just an oddity or oversight of the example that the bounds wouldn't be needed with a concrete type.[1]

(They do say the example is "excessively generic" -- maybe they meant excessively bound.)

Perhaps the example would have been better written like so.

struct Wrapper<T>(T);
impl<AnyIterable> Debug for Wrapper<AnyIterable>
where
    // Fix error from an attempt with no bounds
    for<'a> &'a AnyIterable: IntoIterator,
    // Fix error from an attempt with only the above bound
    for<'a> <&'a AnyIterable as IntoIterator>::Item: Debug,
{
    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
        f.debug_list().entries(&self.0).finish()
    }
}

I don't think the book was thinking about trivial bounds, but...

It happens to be the case that higher-ranked trait bounds are not enforced at the trait implementation site, so you can actually use the concrete version when the bounds aren't met and the implementation will compile (but not be usable unless you add the other implementations). Whereas if you added Self: Clone, say, it won't compile. But for<'a> Self: Clone will! (Always allowing trivially false bounds was RFC'd and tracked here.[2])

So it's a sort of funny situation where the higher-ranked bounds are needed for the concrete implementation to compile when the bounds are not met (and aren't needed when the bounds are met).

But again, I don't think that was something intentional about the example.


  1. Well... assuming the implementation is usable. See the collapsed section below. ↩︎

  2. feature gate trivial_bounds ↩︎

1 Like

Many thanks, @quinedot, for another insightful reply. I greatly appreciate them.

1 Like

Spot on! I was simply trying to give a more complex example of type bounds with higher-ranked trait bounds. The fact that it's on a concrete type was not even on my mind, as I was only focused on the bounds. I'll add this as something to clarify in future releases, thanks!

3 Likes