0

I am trying to improve my understanding of rust borrow checker by making implicit lifetimes explicit. It actually came from a bigger problem for work, but I boiled it down to this (so far).

Let's take this code as an example :

    struct StringWrapper<'a>(&'a str);
    struct StringWrapperWrapper<'a>(&'a StringWrapper<'a>);

    struct ContainingAValue {
        value: String,
    }

    impl ContainingAValue {
        fn do_something_with_wrapper<F>(&self, f: F)
        where
            F: FnOnce(StringWrapper) -> (),
        {
            let wrapper = StringWrapper(&self.value);
            f(wrapper)
        }

        fn do_something_with_wrapper_wrapper<F>(&self, f: F)
        where
            F: FnOnce(StringWrapperWrapper) -> (),
        {
            let wrapper = StringWrapper(&self.value);
            let tmp = StringWrapperWrapper(&wrapper);
            f(tmp)
        }
    }

This code compiles all right. Now, I want to make the lifetimes explicit in the implementation.

    impl ContainingAValue {
        fn do_something_with_wrapper<'a, F>(&'a self, f: F)
        where
            F: FnOnce(StringWrapper<'a>) -> (),
        {
            let wrapper = StringWrapper(&self.value);
            f(wrapper)
        }

        fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
        where
            F: FnOnce(StringWrapperWrapper<'_>) -> (),
        {
            let wrapper = StringWrapper(&self.value);
            let tmp = StringWrapperWrapper(&wrapper);
            f(tmp)
        }
    }

Until there, it also compiles all right.

Now, the big question : what lifetime should I put instead of '_ in the StringWrapperWrapper<'_> of do_something_with_wrapper_wrapper ?

I thought that this would work (line number added for reference in the error):

  17   │     fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
  18   │     where
  19 ~ │         F: FnOnce(StringWrapperWrapper<'a>) -> (),
  20   │     {
  21   │         let wrapper = StringWrapper(&self.value);
  22   │         let tmp = StringWrapperWrapper(&wrapper);
  23   │         f(tmp)
  24   │     }

but I get :

   |
17 |     fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
   |                                          -- lifetime `'a` defined here
...
21 |         let wrapper = StringWrapper(&self.value);
   |             ------- binding `wrapper` declared here
22 |         let tmp = StringWrapperWrapper(&wrapper);
   |                                        ^^^^^^^^
   |                                        |
   |                                        borrowed value does not live long enough
   |                                        this usage requires that `wrapper` is borrowed for `'a`
23 |         f(tmp)
24 |     }
   |     - `wrapper` dropped here while still borrowed

So, I tried to add a different lifetime :

  17 ~ │     fn do_something_with_wrapper_wrapper<'a: 'b, 'b, F>(&'a self, f: F)
  18   │     where
  19 ~ │         F: FnOnce(StringWrapperWrapper<'b>) -> (),
  20   │     {
  21   │         let wrapper = StringWrapper(&self.value);
  22   │         let tmp = StringWrapperWrapper(&wrapper);
  23   │         f(tmp)
  24   │     }

But get exactly the same error (with 'a being replaced by 'b).

The fact that I am using a FnOnce is important for my usecase and the error as this would compile :

    fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
    where
        F: FnOnce(StringWrapperWrapper<'a>) -> (),
    {
        let wrapper = StringWrapper(&self.value);
        let tmp = StringWrapperWrapper(&wrapper);
        // f(tmp)
    }

1 Answers1

0

This is a perfect usecase of higher rank trait bounds. The correct code should be

impl ContainingAValue {
   fn do_something_with_wrapper<'a, F>(&'a self, f: F)
    where
        F: for<'b> FnOnce(StringWrapper<'b>) -> (),
    {
        let wrapper = StringWrapper(&self.value);
        f(wrapper)
    }
    fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
    where
        F: for<'b> FnOnce(StringWrapperWrapper<'b>) -> (),
    {
        let wrapper = StringWrapper(&self.value);
        let tmp = StringWrapperWrapper(&wrapper);
        f(tmp)
    }
}

The idea is that you expect f to work no matter the lifetime of the argument, which means in particular it will work with a lifetime bound by the scope of do_something_with_wrapper_wrapper, which cannot be named outside of do_something_with_wrapper_wrapper.

What you are expressing with

fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
where
    F: FnOnce(StringWrapperWrapper<'a>) -> (),
{
    let wrapper = StringWrapper(&self.value);
    let tmp = StringWrapperWrapper(&wrapper);
    f(tmp)
}

is that the caller of do_something_with_wrapper_wrapper chooses the lifetime that f requires for its argument, which may be longer than the scope of do_something_with_wrapper_wrapper.

jthulhu
  • 7,223
  • 2
  • 16
  • 33
  • You are a life saver. It totally make sense. I didn't know that notation. Thanks a lot! – Xavier Detant Jul 07 '23 at 09:54
  • Here is a follow up question on how to generalize this (and use) : https://stackoverflow.com/questions/76638405/mismatched-types-one-type-is-more-general-than-the-other-using-constrained-hi – Xavier Detant Jul 08 '23 at 13:55