henning
henning

Reputation: 105

Code working with elided lifetimes, not with explicit

The following code works fine:

fn get<F: Fn(&[u8]) -> u8>(f: F) -> u8 {
    f(&[1, 2, 3])
}

However, when I add explicit lifetime information to it, it doesn't:

fn get<'inp, F: Fn(&'inp [u8]) -> u8>(f: F) -> u8 {
    f(&[1, 2, 3])
}

What lifetime does the compiler infer in the working code?

I'm using Rust 1.18.0.

The error message is:

error: borrowed value does not live long enough
 --> test.rs:4:8
  |
4 |     f(&[1, 2, 3])
  |        ^^^^^^^^^ does not live long enough
5 | }
  | - temporary value only lives until here
  |
note: borrowed value must be valid for the lifetime 'inp as defined on the body at 3:49...
 --> test.rs:3:50
  |
3 |   fn get<'inp, F: Fn(&'inp [u8]) -> u8>(f: F) -> u8 {
  |  __________________________________________________^
4 | |     f(&[1, 2, 3])
5 | | }
  | |_^

Upvotes: 3

Views: 154

Answers (2)

Lukas Kalbertodt
Lukas Kalbertodt

Reputation: 88576

Lifetimes in trait bounds are a bit special and the Fn family of traits has a special lifetime elision rule. We'll dive into that, but first, here it the correct explicitly annotated version:

fn get<F: for<'inp> Fn(&'inp [u8]) -> u8>(f: F) -> u8 {
    f(&[1, 2, 3])
}

Oh gosh, what is this for<'inp> doing there? It's a so called higher ranked trait bound (HRTB) and it's used here to make 'inp universally quantiefied in regards to f. In order to fully understand that, we need to understand a bit of theory.


Who has the choice?

Let's take a look at an easy example:

fn bar<'a>(x: &'a u8) {}

Here, bar() is generic of the lifetime 'a. The syntax above reads: "choose any 'a and there is a bar() that will work with 'a". This means that we can choose any 'a we want, and bar() works! Who are "we"? "We" are the caller -- the one calling bar. This will be important later: the caller chooses the generic parameters. We can call bar() with a &'static u8 as well as with a reference that doesn't live as long.

Now you might ask: are there situations where the caller doesn't choose the generic parameter, but someone else does? Yes, there are! Sadly, it's a bit more difficult to understand, because it doesn't occur too often in today's Rust code. But let's try:

trait Bar<'a> {
    fn bar(&self, x: &'a u8);
}

This is similar to the bar() function above, but now the lifetime parameter is defined on the trait, not the function. Let's use the trait:

fn use_bar<'a, B: Bar<'a>>(b: B) {
    let local = 0u8;
    b.bar(&local);
}

This doesn't compile, printing the same error as above. Why? The method b.bar() expects a reference of lifetime 'a. But who chooses 'a here? Exactly: the caller -- the caller of use_bar(), not the caller of bar()! So the caller of use_bar() could choose the 'static lifetime; in that case, it's easy to see that our &local doesn't fulfill the lifetime requirements.

Note that the caller of use_bar() chooses 'a as well as B. Once use_bar() is instantiated, B is a fixed type and B::bar() works only for one specific lifetime. This means the caller of bar() can't choose the lifetime, but bar() itself chose it!

What do we want instead? We want use_bar() to choose the lifetime of the bar() call. And we can do that with the for<> syntax:

fn use_bar<B: for<'a> Bar<'a>>(b: B) {
    let local = 0u8;
    b.bar(&local);
}

This works. What we say here is: "for any lifetime parameter 'a, B has to implement the trait Bar<'a>". Instead of: "there needs to exist a lifetime parameter 'a for which B implements Bar<'a>". It's all about who chooses which parameter.

Let's use the real names for it:

  • a generic parameter is universally quantified if the caller can choose it
  • a generic parameter is existentially quantified if the callee can choose it

What does Rust do?

To return to your example:

fn get<'inp, F: Fn(&'inp [u8]) -> u8>(f: F) -> u8 {
    f(&[1, 2, 3])
}

Here we have the same problem as above: the lifetime parameter of f is existentially quantified. The caller of f cannot choose the lifetime parameter. We can fix that with the for<> syntax as shown above.

When you omit the lifetimes:

fn get<F: Fn(&[u8]) -> u8>(f: F) -> u8 {
    f(&[1, 2, 3])
}

The Rust compiler will do something special for the Fn family of traits. Your F: Fn(&[u8]) desugars to F: for<'a> Fn<(&'a [u8],)>. If you use Fn* traits with parameters that involve lifetimes, those lifetimes are automatically universally quantified, because that's usually what you want with higher order functions.

Upvotes: 4

henning
henning

Reputation: 105

The infered code seems to be:

fn get<F : for<'inp> Fn(&'inp[u8]) -> u8>(f: F) -> u8 {
    f(&[1,2,3])
}

Explanation

Upvotes: 0

Related Questions