6

I think I understood how lifetimes work with function parameters/outputs and with structs, since those cases are explained in the book (and further in the nomicon), but I do not understand how lifetimes are used for type bounds. For example:

trait Min<Elmt> {
    fn get_min(&self) -> Elmt;
}

impl<T, Elmt> Min<Elmt> for T
where
    &T: IntoIterator<Item = Elmt>,
    Elmt: Ord,
{
    fn get_min(&self) -> Elmt {
        let mut iter = self.into_iter();
        match iter.next() {
            None => panic!("Empty List"),
            Some(x) => iter.fold(x, |acc, y| acc.min(y)),
        }
    }
}

Here I require the type T to fulfill the following property: Any of its references must implement IntoIterator<Item = Ord>. This code does not compile because Rust wants &T to have a lifetime and I can not figure out why. The where clause should only ensure that something is of a certain type, which is done at compile time. How do lifetimes/references come into this? Ensuring that &T of an implementing type T implements IntoIterator has nothing to do with dangling pointers or memory leaks.

Someone from the Rust Discord helped me by making this working implementation:

trait Min<'a, Elmt> {
    fn get_min(&'a self) -> Elmt;
}

impl<'a, T, Elmt> Min<'a, Elmt> for T
where
    &'a T: IntoIterator<Item = Elmt> + 'a,
    Elmt: Ord,
{
    fn get_min(&'a self) -> Elmt {
        let mut iter = self.into_iter();
        match iter.next() {
            None => panic!("Empty List"),
            Some(x) => iter.fold(x, |acc, y| acc.min(y)),
        }
    }
}

(Playground variant)

They tried to explain it to me, but I could not understand, so I am looking for another explanation to find its way into my brain.

Let me go through it one by one:

  1. &'a T: IntoIterator<Item = Elmt> + 'a, we give the generic reference &T a lifetime 'a. Why is this needed?
  2. Why do we have to restrict &'a T to the lifetime 'a? I mean isn't that kind of a tautology?

    Since we used a lifetime in the where clause, we have to add it to some signature. That is where impl<'a, T, Elmt> comes from - fine

  3. Why does the trait Min and the get_min() function need a lifetime parameter now? Lifetime elision should give every input element a lifetime and the output does not have a lifetime anyway.

I think it might help if I knew what traits are/how they are implemented. Objects are basically a list of variable and method pointers. Do the methods from traits simply get appended to the method pointers of every implementing object? Are they separate objects somehow? Do they actually have a lifetime? Which lifetime? I am never assigning the "general Min trait" by using let a = Trait::new() syntax, so how can it have a lifetime?


Addressing the for<>-implementation comments:

While the trait and implementation would compile with a for<'a> lifetime bound on &T, the result is not usable. Since &Vec apparently does not fulfill the trait bounds then (cf. this playground). This was also brought up in the Discord discussion, and I also do not understand the for<> syntax yet, but I thought that this would maybe be out of scope for this question.

Felix B.
  • 905
  • 9
  • 23
  • That _may_ not even be the right solution. See [How do I write the lifetimes for references in a type constraint when one of them is a local reference?](https://stackoverflow.com/q/44343166/155423) and [How does for<> syntax differ from a regular lifetime bound?](https://stackoverflow.com/q/35592750/155423). – Shepmaster May 13 '20 at 19:57
  • I think your question is answered by the answers of [Why are explicit lifetimes needed in Rust?](https://stackoverflow.com/q/31609137/155423). If you agree, we can mark this question as already answered. – Shepmaster May 13 '20 at 19:59
  • TL;DR the proposed duplicate: because explicit is usually better for understanding. – Shepmaster May 13 '20 at 19:59
  • 1
    Okay no "why are explicit lifetimes need in Rust" is explicitly NOT answering my question. I do understand the point of them for function parameters/outputs and structs (as said in the question). But I do not understand them in the context of traits and type restrictions – Felix B. May 13 '20 at 20:00
  • The first comment is orthogonal to the thrust of _this_ question (only matters for the solution you got) , and it may not even be applicable. The second comment is much more relevant. – Shepmaster May 13 '20 at 20:02
  • See also [Why is adding a lifetime to a trait with the plus operator (Iterator + 'a) needed?](https://stackoverflow.com/q/42028470/155423) – Shepmaster May 13 '20 at 20:04
  • @Shepmaster that particular `+ 'a` seems unrelated to this, since there Box actually defines the lifetime as `'static` by default. In this case we literally name the lifetime of `&T` `'a`, but for some reason have to restrict `&T` to the lifetime which it already has – Felix B. May 13 '20 at 20:36
  • I think you are getting confused because you're trying to write code that is confusing, not because of anything to do with lifetimes or traits in general. In particular, it is not at all obvious to me why `Elmt` should be a parameter to `Min`. If you allow `Elmt` to be an associated type instead, [it's much easier to write `Min` in a way that makes your `main` compile.](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8e5a8ffc6b2e50cf07c84ab3e557b27d) – trent May 13 '20 at 20:57
  • @trentcl well, the trait was not my idea, it is the trait from the statrs crate: https://github.com/boxtown/statrs/blob/master/src/statistics/traits.rs. If you search for statistical crates, you would likely come across this: https://www.reddit.com/r/rust/comments/5qv11u/what_is_the_best_statistics_crate/ where 3 packages (statrs, statistical, streaming-stats) are named, of which statrs has the most downloads. So in some sense it is the default statistical crate for rust. I just wanted to play with it and maybe try to extend it. Right now I do not want to obtain working code, but understand it – Felix B. May 14 '20 at 07:02
  • @trentcl but on that note: what is the difference between `trait` and `trait { type Elmt...`? I am guessing that the first version would result in multiple versions of the trait, so you could implement `Min` for `Vec` as well as `Min` for `Vec`, while the second variant would have you chose the output type when implementing Min once and for all. Correct? – Felix B. May 14 '20 at 07:10
  • Yes, in a sense generics (`trait Min`) are *input types* and associated types (`trait Min { type Elmt; }`) are *output types*. There is another Q&A on this topic: [When is it appropriate to use an associated type versus a generic type?](https://stackoverflow.com/q/32059370/3650362) but the answers may be a bit dense. Your summary is essentially correct. – trent May 14 '20 at 12:38
  • Just my humble observation: the main reason around of the issue is that "something" with a type `Elmt` is getting out of shared reference (`fn get_min(&self) -> Elmt`), that is what Rust is worrying about, and `Elmt` can have a lifetime of `&self` (like `'a` lifetime) as well as a `'static` lifetime... Seems like trait has a `'static` lifetime by default, while it would be `'a` for a function... So it's kinda confusing to elide. And there is no problem if `self` is moving into `get_min` without a reference like `get_min(self)`, so Rust doesn't worry that `Elmt` outlives the `self`.. – Alexander Fadeev May 14 '20 at 17:48
  • @AlexanderFadeev that sounds plausible, but I am not sure why it is related to `&T`. If you remove the `&` from the `&T` (i.e. implement it for types which implement `IntoIter`, instead of for types whose references implement `IntoIter`) Rust does not complain about lifetimes. But the `&self` and `Elmt` issue stays. – Felix B. May 14 '20 at 18:12
  • @FelixB. Yeah, because if `self` is taking out its life into `get_min` there is no reason to control it's lifetime: there is no lifetime, it's gonna be dead in any case. So whatever goes out the function will be legit: either something new is created inside the function, or the part of the `self`. – Alexander Fadeev May 14 '20 at 18:23
  • @AlexanderFadeev you could do that without changing `&self` to `self`, cf. https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=76c1604f3306ddd6039fe3ceef24f585 – Felix B. May 14 '20 at 18:29
  • @FelixB. Oh, this is because you added a `Copy` trait. Remove it, and you will see that Rust tries to make a `*self` + copy at the place of `into_iter`, but it can't. P.S.: with copying it's safe, right, but it differs from your original example. – Alexander Fadeev May 14 '20 at 18:36
  • @AlexanderFadeev yes I added the Copy trait since I wanted it to compile, and then we have a function with an `&self` in and a (potential reference) `Elmt` out. So the situation you referred to as problematic in your first comment. But it compiles fine. But if I add a `&` back at the `T` it stops compiling: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=179f458f4c8218091ed962c2cab0ff6e That is why I wonder why this is related. – Felix B. May 14 '20 at 18:43
  • @Felix B. As I noticed when you put `T: ` it tries to deref `*self` and copy, and when you have `&T: ` it doesn't deref and you have to put lifetime, and we come back to initial situation. And why Rust knows that it should to get derefed and copied in the first case: I don't really know, though I can guess, but this is just another story... :) – Alexander Fadeev May 14 '20 at 18:51

0 Answers0