Lifetime woes: How can I borrow an Option<&T> but return a T?

Hi everyone! I read the forum basically every day, trying to learn new things. Thanks for a great and engaging community. I think I've nailed the basics of Rust, but am now starting to scratch the surface of Lifetimes.

Anyway.

I am trying to write a test help function that takes a Fn that borrows a field, and then returns a clone, for convenience. But the borrow checker is not having any of it!

#[test]
fn timestap_deserialize_serialize() -> Result<(), Box<dyn Error>> {
    let date_str = "2021-01-02T12:53:39.392Z";
    let mut value_to_deserialize = DatastoreValue::empty();
    value_to_deserialize.timestamp_value = Some(date_str.to_string());
    let date_time = NaiveDateTime::deserialize(value_to_deserialize)?;
    let serialized_value: String =
        get_datastore_value(date_time, |d| d.0.timestamp_value.as_ref())?;
    assert_eq!(date_str, serialized_value);
    Ok(())
}

fn get_datastore_value<'a, T, F>(
    serializable: impl Serialize,
    select_prop: F,
) -> Result<T, Box<dyn Error>>
where
    T: 'a + Clone,
    F: Fn(&'a DatastoreValue) -> Option<&'a T>,
{
    match serializable.serialize()? {
        None => return Err(Box::new(TestError::Unknown)),
        Some(serialized_value) => {
            let selected_prop = select_prop(&serialized_value);
            match selected_prop {
                None => return Err(Box::new(TestError::Unknown)),
                Some(serialized_value_2) => Ok(serialized_value_2.clone()),
            }
        }
    }
}

I think that my problem is that the 'a in T: 'a + Clone requires T to obey the lifetime of 'a even though i clone it?

I know I can solve it by removing all the lifetimes and just moving out of the closure, but I wanted to learn more about borrowing.

Is it possible to "fix" get_datastore_value and still keep borrowing?

Here is the error message:

error[E0597]: `serialized_value` does not live long enough
   --> tests/entity/main.rs:140:45
    |
129 | fn get_datastore_value<'a, T, F>(
    |                        -- lifetime `'a` defined here
...
140 |             let selected_prop = select_prop(&serialized_value);
    |                                 ------------^^^^^^^^^^^^^^^^^-
    |                                 |           |
    |                                 |           borrowed value does not live long enough
    |                                 argument requires that `serialized_value` is borrowed for `'a`
...
145 |         }
    |         - `serialized_value` dropped here while still borrowed

Here is the code

Thanks!

1 Like

Does this signature work?

fn get_datastore_value<T, F>(
    serializable: impl Serialize,
    select_prop: F,
) -> Result<T, Box<dyn Error>>
where
    T: Clone,
    F: for<'a> Fn(&'a DatastoreValue) -> Option<&'a T>,
{
5 Likes

YES! :heart_eyes:
Thank you alice! I think I've read about 5 posts where you use for<'a> magic lately!

I was actually thinking before I posted: "either alice is gonna solve it, or alice will say something about GATs" :joy:

The reason yours didn't work is that your closure could only be called with one specific lifetime 'a, and that lifetime was chosen by the caller, so your method had to be valid no matter what 'a was. So, if the caller chooses 'a = 'static, then your borrow of select_prop must last for the 'static lifetime, which it clearly doesn't.

With for<'a> we say that the closure should be callable with all lifetimes.

4 Likes

... does for<'a> (fall all lifetimes) mean the caller has to supply a function that only uses 'static?... or is it "for all lifetimes 'a: 'a ? As I write this out, thinking out-loud, it seems to be the only useful way to interpret it. Yes?

Nope, just that the function has to be generic over that lifetime, e.g. it can't expect a specific lifetime

Thank you. From the caller's perspective, I (caller) can provide a function that is generic over 'a. Does generic over any 'a include no borrow at all; so pass by value? What about a situation where the lifetime of the input is 'a, but the output passes ownership to a new alloc? i.e., where 'output: 'input by definition of output not having a borrow?

If I understand your questions correctly, the answer to both is "yes".

If how I'm thinking about it is correct, then the for<'a> only requires that the lifetime of the input effectively last as long as that of the output. I'm hanging on whether it implies some relationship between the two lifetimes when "how it reads" is "must be valid for all 'a; seems clear from the function being called (the one that accepts a function as an argument).

What it means for the caller however includes a broader set of options because the relationship between the input and output lifetimes do not have to be one in the same. I have described a situation where it would seem valid even when dealing with 2 different lifetimes from the caller's perspective. 'a:'a works, as does 'output: 'input, but not when output ~ 'static because it would point to deallocated memory (that seems like an oxymorone; deallocating 'static). Only when we interpret "pass by value/ownership" as living as long as is ever required.

In drafting out all what one can do as a caller of the function with the spec for<'a>, has me trying to conceptually align the idea of ownership with lifetimes. It's not all squaring just yet because normally if my function returns a borrow, the lifetime has to be tied to that of the input (at minimum 'a:'a, but if input:output that works too unbeknownst to the implementation). I believe if I allocate memory and pass ownership of that memory in the return value of the function, I would get a type error; "expecting &'a T, got T". Thus the question about for <'a> which seems to permit both scenarios... something I can confirm tomorrow :))

for<'a> means any lifetime can be "chosen" for 'a. As for relationships between the input and output (or multiple inputs, or multiple outputs) -- that is determined by whether or not they have the same name, and any 'a: 'b-type "outlives" bounds which are present.

As for the rest of your post, I doubt I'll understand without some concrete examples. Though you may find this resource on lifetime misconceptions informative -- the first misconception shows how a generic can except both a reference and an owned type, for example.

I will read the reference tomorrow (thank you!). But, in the meantime, so yes, a generic can indeed except both a reference and an owned type? Or is the misconception that it in fact cannot.

It can.

fn my_print<T: core::fmt::Display>(value: T) {
    println!("{}", value);
}

fn main() {
    my_print(42);
    my_print(&7);
}

You're awesome for spelling it out like that. Thank you. The point is taken and confirms that it was fair to think more about the for <'a> semantic.

My only FYI with trying to contrast the behavior of a borrow with a move semantic, is that using primitives that Rust "automagically" calls Copy obfuscates what goes on otherwise. It might be completely moot here, but that only reflects my need to confirm what is not yet instinctive.

Thank you again for helping me to consolidate a few things.