#Question about HRTBs in impl of a struct with function traits

1 messages · Page 1 of 1 (latest)

slim flume
#

In the rust nomicon, an example of a HRTB being used is given:

struct Closure<F> {
    data: (u8, u16),
    func: F,
}

impl<F> Closure<F>
    where for<'a> F: Fn(&'a (u8, u16)) -> &'a u8,
{
    fn call(&self) -> &u8 {
        (self.func)(&self.data)
    }
}

fn do_it(data: &(u8, u16)) -> &u8 { &data.0 }

fn main() {
    let clo = Closure { data: (0, 1), func: do_it };
    println!("{}", clo.call());
}

I roughly understand that the statement where F: for<'a> Fn(&'a (u8, u16)) -> &'a u8, is meant to relate the lifetime of the argument of the function trait to the returned reference in a generic way, a bit like universal quantification. However, what I don't understand is why the lifetime cannot simply be specified in the impl:

impl<'a, F> Closure<F>
    where F: Fn(&'a (u8, u16)) -> &'a u8,
{
    fn call(&self) -> &u8 {
        (self.func)(&self.data)
    }
}

In the similar example of a lifetime specified in a function instead of with HRTBs:

pub fn foo<'a>(x: impl Fn(&'a i32) -> &'a i32) -> i32 { let y = 42; let z = x(&y); *z }

I roughly understand that this does not work because the lifetime 'a has an implicit bound set by the function foo i.e. 'a must be longer than the function body. This thus means that this function cannot compile, as it does not account for lifetimes shorter than the lifetime of foo within the function body.

However, I don't understand why the same cannot be true of the struct impl for Closure. This stackoverflow link seems to suggest that lifetime annotations placed in impl can be thought of like generics, but for lifetimes: https://stackoverflow.com/questions/39355984/what-does-the-first-explicit-lifetime-specifier-on-an-impl-mean. Thus, since the lifetime 'a is not bounded by anything, unlike the example of the function, I would expect 'a to be parameterised correctly to the function trait, just like with the HRTB. Instead, the rust compiler gives this error:

-continued below-

#
error: lifetime may not live long enough
  --> src/main.rs:10:9
   |
6  | impl<'a, F> Closure<F>
   |      -- lifetime `'a` defined here
...
9  |     fn call(&self) -> &u8 {
   |             - let's call the lifetime of this reference `'1`
10 |         (self.func)(&self.data)
   |         ^^^^^^^^^^^^^^^^^^^^^^^ method was supposed to return data with lifetime `'1` but it is returning data with lifetime `'a`

error: lifetime may not live long enough
  --> src/main.rs:10:9
   |
6  | impl<'a, F> Closure<F>
   |      -- lifetime `'a` defined here
...
9  |     fn call(&self) -> &u8 {
   |             - let's call the lifetime of this reference `'1`
10 |         (self.func)(&self.data)
   |         ^^^^^^^^^^^^^^^^^^^^^^^ argument requires that `'1` must outlive `'a`

My main question: Why is the HRTB required to specify the lifetime on the function trait here? Why does a generic lifetime specified in impl not work, and why does it fail with the error "lifetime may not live long enough"?

small umbra
#

Because &self is implicitly &'b self, and nothing says that 'b is longer than 'a

slim flume
#

Possible answer I thought of: Is it due to the "generic" lifetime 'a only specifying one specific lifetime? The for<'a> is required for truly generically specifying the lifetime bound in the way I might expect?

small umbra
slim flume
#

In that case, what's the difference between this example, which compiles:

impl<'a, 'b: 'a, F> Closure<F>
    where F: Fn(&'a (u8, u16)) -> &'a u8,
{
    fn call(&'b self) -> &'a u8 {
        (self.func)(&self.data)
    }
}

and the HRTB above:

impl<F> Closure<F>
    where for<'a> F: Fn(&'a (u8, u16)) -> &'a u8,
{
    fn call<'elided>(&'elided self) -> &'elided u8 {
        (self.func)(&self.data)
    }
}

Is it the lifetime being generic over all possible arguments as compared to one specific "generic" lifetime 'a?

small umbra
#

Oh wait, hang on

#

There probably is one, lemme think

slim flume
small umbra
#

But you put 'elided in the wrong place

#

(which doesn't really change anything, here, but it's supposed to go on the function)

slim flume
#

Ah, right

#
// compiles
pub fn foo(x: impl for<'a> Fn(&'a i32) -> &'a i32) -> i32 {
    let y = 42;
    let z = x(&y);
    *z
}

// does not compile
pub fn foo<'a>(x: impl Fn(&'a i32) -> &'a i32) -> i32 {
    let y = 42;
    let z = x(&y);
    *z
}

// compiles, but different from 1
pub fn foo<'a, 'b: 'a>(x: impl Fn(&'a i32) -> &'a i32, y: &'b i32) -> i32 {
    let z = x(y);
    *z
}

Not sure if this is relevant

small umbra
#

In particular, the last foo can be called on y but can't be called on a reference to a value created inside the function