I'm learning rust by playing a bit by building a small server over TCP. I'm using JSON as serde data format. I have a simple generic function read_request as follow:
struct Server;
impl<'a> Server {
[...]
fn read_request<T>(stream: &TcpStream) -> T
where
T: Deserialize<'a>
{
let mut reader = std::io::BufReader(stream);
let raw_request = reader.fill_buf().expect("reading failed").to_vec();
serde_json::from_slice(&raw_request).expect("couldn't parse request")
}
}
As you may have guessed, raw_request doesn't outlive lifetime 'a and to make this work (I believe) I would need to somehow allocate the raw_request in the same (or the above) scope as the Server object. But isn't there a simpler way? And also why does serde_json need to keep raw_request alive even after read_request has returned? Isn't it creating an object of T with freshly allocated data?
- T: Deserialize<'a>
+ T: for <'b> Deserialize<'b>
You need to restrict the caller to supply a type T that doesn’t place any lifetime requirements on the data it serializes from. Deserialization needs to work for any arbitrary lifetime, unknown to the caller.
I see, that makes a lot of sense now... I was defining a lifetime 'a onto the Server, and restricting the T type to have the same lifetime as the Server. Therefore letting the caller choose the lifetime of the temp data.
By making more research I came across the higher rank trait bounds. This is a bit confusing, does it mean that using hrtb, we can remove constraints of the caller choosing the lifetime by assigning an arbitrary one?
It gives the caller less choices, not more. But it's more about the needs of the callee, IMO.
'de in Deserialize<'de> represents how long you need to borrow the serialized data. With the single lifetime version T: Deserialize<'a>, 'a is some borrow duration the caller chooses, longer than the function body. You can't borrow locals for longer than the function body -- they always drop or get moved by then. But borrowing from a local to deserialize is exactly what you want to do!
The tool we have for saying "I need this capability for some lifetime shorter than my function body" is requiring the capability for all lifetimes -- using HRTBs.
Or for an alternative take in terms of the function trait examples you found, maybe think of it as:
// T: Deserialize<'a> means something akin to this exists
fn read(&'a str) -> T
// T: for<'b> Deserialize<'b> means something akin to this exists
fn read<'b>(&'b str) -> T
// Same thing with elision
fn read(&str) -> T
Here T is some single type the caller has chosen. For the second case, we can input any lifetime 'b and we get out the same type T. Types that differ by lifetime are distinct types, so we know T can't have 'b in its type -- it can't be holding on to the borrow that gets passed in.
But for the first case, we only required that the notional function exists for a single lifetime 'a. So it's possible that T does contain 'a.