Also, once a generic parameter is introduced, such as a generic lifetime parameter (e.g., <'a>
), then, like any other parameter, specific choices of the "value" of that parameter are made by the caller. So, when writing a generic function's signature, you have to think you (the callee) don't control the generic parameters.
What you, the callee, control, is the rest of the function signature, that is, the contract expressed by that signature, and the constraints that go with it.
So, if you write:
fn first_word<'a> (s: &'_ str) -> &'a str
then, what that signature is saying is that the return value is a borrow tied to the caller-provided 'a
lifetime, whatever that 'a
may be. The caller could, for instance, provide 'a = 'static
, since _there is no constraint preventing them to do so. So, in this case, that signature is equivalent to:
fn first_word (s: &'_ str) -> &'static str
(and if you tried to implement any of those two signatures, you will notice that you will "only" be able to return a string literal "…"
(and that you cannot return s
)).
So, the trick / the idea to keep these caller-provided parameters under control / under check is to constrain those by binding those lifetime parameters to actual function parameters, by tying those together, by using a generic lifetime parameter with a function argument:
fn first_word<'a> (s: &'a str) ...
// ^^ ^^^^^^^^^^
// this |
// parameter… |
// … is bound to
// this argument
This means that while the caller could still choose to pick something like 'a = 'static
; this time, however, they would also have to be providing an s: &'static str
that goes with it. More generally, any lifetime 'a
that they choose would have to be the lifetime of the borrow of *s
, (or a subset of it), which in practice boils down to "'a
is now the lifetime of the input borrow".
This is how reasoning with lifetimes works: by using an initially "free" lifetime parameter (such as <'a>
in the example) in one (or more!) inputs, such parameter must now become "the lifetime of that input (or of those inputs)". This, in turn, allows to then use such a lifetime parameter in return position to, this time, tie the lifetime of your returned value to that very lifetime.
fn first_word<'a> (s: &'a str) -> &'a str
// ^^ ^^^^^^^^^^ ^^^^^^^
// this | |
// parameter… | |
// … is bound to |
// this argument… |
// … and the return value
// is also tied to that
// lifetime parameter:
// the return value is thus
// bound to that first argument,
// "the return value borrows from s"
This, in general, can be hard to understand, because the lifetime parameters are very badly named in the book.
Since, as I mentioned before, a lifetime parameter ought to be used in some function argument, I find it quite useful to have that lifetime parameter be named as that function argument:
fn first_word<'s> (s: &'s str) -> &'s str
- This is the same signature as the one just above it, and yet better conveys that the return value has the lifetime of its
s
argument.
Let's see a more interesting example, the needle and haystack problem: we have two strings, a needle
and a haystack
, and we would like to know if the haystack contains the needle (i.e., if the latter is a substring of the former), and if so, to get the (first) position of the needle inside the haystack:
fn f<'haystack, 'needle> (
haystack: &'haystack str,
needle: &'needle str,
) -> Option<usize> // optional index
{
// naive implementation:
let mut char_indices = haystack.char_indices();
loop {
let remaining = char_indices.as_str();
if let Some((pos, _)) = char_indices.next() {
if char_indices.as_str().starts_with(needle) {
return Some(pos);
} // else { continue; }
} else {
return None;
}
}
}
Now, that works, but what if, instead of returning just a position, we returned a borrow from the haystack
string? In that case, the signature ought to express that the return value borrows from the haystack, and not the needle. Well, this is easy, we just have to apply the ideas discussed here:
Finally, an interesting more advanced lifetime example, is the .as_str()
method on CharIndices
I have used in my haystack example: as you can see, the CharIndices<'a>
itself contains that lifetime parameter <'a>
which is actually the lifetime 'char_indices
of the initial call, i.e., the lifetime of the source 'string
(all these are valid and better names than 'a
, so use whichever you prefer):
impl<'orig_string> CharIndices<'orig_string> {
fn as_str<'as_str_call> (
self: &'as_str_call CharIndices<'orig_string>,
) -> &'orig_string str
...
}
As you can see, with lifetimes, we have been able to express the more advanced notion that the yielded string is not directly borrowing the CharIndices
instance itself, but rather, the original string (so that this CharIndices
instance can be trashed, moved, mutated, that the obtained .as_str()
will remain valid).