Help me to understand this code: trait bounds

Can anyone help me to understand below code from Rust for Rustceans book?

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()
}}

And I have a question: Do you know any blog post or resource that explained trait bounds comprehensively?

Let's start with the basic framework:

impl Debug for AnyIterable // How to debug print an `AnyIterable`
where ... {                // ... But it's only possible sometimes
    // Required method
    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { ... }
}

As for the bounds, there are two:

// Any reference to `AnyIterable` must be iterable...
for<'a> &'a Self: IntoIterator,

// ... and the items yielded by that iterator must implement `Debug`
for<'a> <&'a Self as IntoIterator>::Item: Debug

These capabilities are then used within fmt implementation. In particular, they match the requirements imposed by the argument to DebugList::entries().

The for<'a> part is known as a higher-ranked trait bound and is necessary in this case because the reference &self doesn't have a lifetime that can be named in the bounds— It could be anything, and so we need to make sure the code is valid for everything.


Not off the top of my head, but the key thing to remember is that they have a dual purpose:

  • Seen from the outside, they are the requirements that you need to fulfill in order to use the guarded code.
  • Seen from the inside, they are the capabilities that you are allowed to use in your implementation.

Combined, these two ensure that any uses of this code that the trait solver accepts will actually result in functional code. This is stricter than something like C++'s template system, with the aim of making the compilation errors easier to understand and reason about (because they should all arise from a violation of an explicit contract instead of an implicit one).

8 Likes

Thanks @2e71828, nice explaination.
I don't understand this part yet:

That's relatable, I think lots of people take some time to get their head around it and build an intuition.

If you're familiar with mathematics at all, it reads a lot like how proofs might be stated.

for<'a>

Is basically saying:

For any lifetime 'a

The way it works is that, rather than saying:

where &'a Self: IntoIterator

And then the compiler being confused because it doesn't know what lifetime 'a you're talking about.

We explicitly say, I don't care what specific lifetime 'a is... for any lifetime, this bound needs to hold.

Which is written:

where for<'a> &'a Self: IntoIterator
5 Likes

We can also ask, what's the difference between these?

fn foo<T>(t: &T) where for<'a> &'a T: Trait {}

fn bar<'b, U>(u: &'b U) where &'b U: Trait {}

One difference is that bar only talks about a single lifetime ("exists 'b such that..."), which can be important for higher-ranked bounds. But there is another crucial difference.

With bar, the caller chooses the lifetime 'b, and the function body has to work for every possible choice -- like 'static, say. In particular, you can't assume they'll choose a local lifetime,[1] so the function body can never assume the borrow of a local meets the bound. (Therefore it's generally only useful if the caller also supplies a &'b U.)

In contrast the bound on foo does apply to local borrows. Sometimes this is phrased as applying to arbitrarily short lifetimes (shorter than the caller can name). This can matter when you need things to work for local lifetimes, like when you need to pass a reference with the lifetime due to the trait definition as one example.


In my experience it's more common to have a related scenario, where instead of bounds on references, you have bounds on type parameters involving traits with a lifetime parameter.

(Then things become a bit more complicated when you have implementations of traits with lifetimes for types that also have lifetimes...)


  1. in fact they cannot choose a local lifetime ↩︎

3 Likes

If you're not familiar with mathematics, it might help to think of 'a in for<'a> as roughly being "the worst lifetime you could possibly get". Often, that's the longest lifetime 'static or the ephemeral lifetime (no syntax), but sometimes it's neither.

for<'a> in trait bounds can be especially unintuitive, as extra syntax in a trait bound would usually strengthen it, causing some types to no longer fulfill it. But the semantics of for<'a> doesn't do that. Instead, it expresses a lack of outlive relations.

Normally, named lifetimes in trait impls are linked to a part of the thing you're implementing the trait on.

For example, the trait impl below is saying that a borrow of Self has to meet the trait bound, but only if it lives as long as 'a[1].

impl <'a> Trait for Struct<'a>
    where &'a Self: RequiredTrait
{
    fn foo(&self) {
        // Here, the elided lifetime in &self expands to
        // &'shorter_than_a Self, which may as well not
        // implement RequiredTrait.

        // Therefore, trying to call RequiredTrait's method on
        // self would result in a compile error.
    }
}

In contrast, for<'a> introduces a "fresh" lifetime, opting out of this linkage to another, already-established lifetime.

Try it out here.


  1. 'a here is not the liveness scope of Self: Instead, it matches another &/&mut inside Self. ↩︎

2 Likes

A question came to mind! (Apologies, I'm still a beginner!)
Assume this trait:

trait Trait {
    fn method(self) where Self: Sized, {
    }
}

In the following function, I can’t call t.method() and it's obvious, because method takes ownership of self.

fn foo<T>(t: &T) where T: Trait {
    t.method();
}

But do the functions below solve this issue?

fn bar<T>(t: &T) where for<'a> &'a T: Trait,
{
    t.method();
}

fn baz<'a, T>(t: &'a T) where &'a T: Trait {
    t.method();
}

How is it possible to call method on &T?

These are the same:

fn method(self);
fn method(self: Self);

And Self is an alias for the implementing type.

Rust references are full-fledged types, so if a reference is the implementing type, then method's receiver (Self) is a reference.

That's why the snippets you wrote which work are possible: the bounds say that the implementing type is a reference, and you have a reference to call the method with. Whereas when (by the bounds) the implementing type was T and you had a &T (and T was not Copy), you couldn't get a T to call the method with. The types didn't line up.

Make sense?

2 Likes

So self here is actually &T. Did I get it correctly?
I have two questions:
In the following code I think t should be moved because method consumes t(because of self), but the code works fine, why?!

fn bar<T>(t: &T) where for<'a> &'a T: Trait,
{
    t.method();
    t.method();
}

And in the following code I get error that '&' without an explicit lifetime name cannot be used here, and here is that we will need using lifetimes explicitly. firstly why do we need explicitly define lifetimes here?

fn foo<T>(t: &T) where &T: Trait {
    t.method();
}

Thanks for your patience, @quinedot !

Here t is &T, a shared reference, and shared references are Copy. So t doesn't get consumed. [1]

It helps to remember that &T is not, in fact, a single type but rather a family of types. It's a convenience notation for &'a T for some lifetime 'a. Pertinently, if &T occurs twice in a function signature, the two instances don't necessarily refer to the same &'a T. But in several common cases the compiler allows you to elide the lifetime annotations and infers that you most likely want two instances of T& to have compatible lifetimes. As such, for example, this signature:

fn foo(&T) -> &U

is in fact short for

fn foo<'a>(&'a T) -> &'a U

because typically such a signature is a projection (getter) of some sort, that is, it returns a reference to a field of T, which naturally shares the lifetime of the containing object.

But here

there is no lifetime elision and thus the where clause by default doesn't bound the argument type in any way -- there's no single "overwhelmingly likely" option for what the lifetimes should be, so the compiler requires you to be explicit.


  1. This works if the called method takes &self; if it took self this wouldn't compile because you can't consume (move out) stuff through a shared reference. ↩︎

3 Likes

In this particular example, the bounds say that Trait (which provides method()) is implemented on &T. This allows a self-taking method to be used because the Self type (&T) implements Copy.

3 Likes

Yes, self is &'s T for some particular lifetime 's.

// `&'x T` has to implement `Trait` for every `'x`
fn bar<T>(t: &T) where for<'a> &'a T: Trait,

// `&'x T` has to implement `Trait` for `'a` specifically
fn baz<'a, T>(t: &'a T) where &'a T: Trait {

Because all shared references (&T) implement Copy.[1]

It could be special-cased to have some meaning, but it's not as straight-forward as it may seem at first. Even if we stick to freshly introduced "any lifetime" bindings there are sometimes multiple possibilities:

where &T: Trait
// where for<'a> &'a T: Trait

where &T: Trait<'_> 
// where for<'a> &'a T: Trait<'a>
// -or-
// where for<'a, 'b> &'a T: Trait<'b>

where &Ref<'_, T>: Trait<'_>
// etc

But also because it could be confusing and easy to make mistakes in context:

fn context<'a, T>(_: &'a T)
where
    &T: Trait,
    // for<'t> &'t T: Trait
    // -or-
    // &'a T: Trait

The two bounds in the last code block put different restrictions on the caller and callee. But if &T: Trait desugared to for<'t> &'t T: Trait, the two bounds are only two easy-to-forget (and overlook) characters away from each other.

Being familiar with the current function lifetime elision rules, I'm not even sure which desugaring is more intuitive -- which probably means "neither is, so it's not a good candidate for sugar".


  1. The ability to use "always true" trait implementations isn't particular to references or Copy by the way. ↩︎

1 Like

This has been covered by some commenters above, but I want to add that idea of the self receiver consuming or transferring ownership is just an approximation that some explanations will start with.

Your understanding could shift to the self receiver being a move without consideration for transfer of ownership. It was mentioned that &T is a Copy type, and the key to recognize here is that Copy types can always be moved. Coming full circle, when the trait is implemented on &T, the self receiver is &T.

1 Like
trait Trait {
    fn method_1(self) where Self: Sized, {
        // self -> &'a T
    }

    fn method_2(&self) where Self: Sized, {
        // self -> &&'a T??
    }
}
fn bar<T>(t: &T) where for<'a> &'a T: Trait,
{
    // t.method_1();
    // t.method_2();
}

If for the t.method_1(), self is &'a T. in the method_2 we will have &&'a T for 'self`? So what does it mean?!

If the implementing type is &T, then a &self receiver will be a &&T, yes. References compose like any other type (you can have a Vec<Vec<T>> too). I'm not sure what else to say about it.

3 Likes

It means that calling method_2 is awkward if you have an owned value t:

        fn bar2<T>(t: T)
        where
            for<'a> &'a T: Trait,
        {
            t.method_1();
            (&t).method_2();
        }

Luckily I think this rarely comes up. I've don't think I've ever had to do it.

1 Like