Welcome the module 4, An exploration of generic lifetimes. In this module, we'll discuss what a generic lifetime is,
as opposed to the concrete lifetimes we discussed in modules 2 and 3.
We'll go through three examples that return references and illustrate generic lifetimes:
a function with one parameter
that's a reference,
a function with two parameters
that are references, and a method with a parameter
that's a reference.
Let's start with defining what we mean by "generic lifetime."
A generic lifetime is the lifetime of a reference in code, where we can't know all of the possible concrete lifetimes of the values being referenced at compile time.
Generic lifetimes can exist in the definitions of functions,
methods, structs, enums, and traits.
In this video,
we're going to cover generic lifetimes in functions and methods.
To compile code involving generic lifetimes, Rust needs to have enough information to prove that the references will be valid for any concrete lifetime of any value the definition is invoked with.
Now,
let's look at some examples involving generic lifetimes,
starting with a function with one reference as a parameter,
and that returns a reference.
We're actually going to look again at the example from the module on concrete lifetimes,
with the return_first_two function.
When we looked at the return_first_two function before, we annotated the lifetimes of the references in the parameter and return value with an orange dotted line because they're tied to some value outside of the function.
Then, we looked at where we called the return_first_two function in main and filled in the orange lifetime with blue because it was connected to the list value in main.
The orange lines actually represented a generic lifetime,
but we glossed over that detail in the earlier video.
In that video,
we were able to look at main,
where return_first_two was called,
and see that the concrete lifetimes were valid.
However,
the Rust compiler only ever analyzes lifetimes and functions in isolation because it needs to guarantee that references will be valid in any call to the function,
not only the calls currently available to the compiler.
For instance,
if the return_first_two function is part of the public
API of a crate that we publish on crates.io,
Rust has no way of knowing all of the concrete ways people downloading the crate in the future
will call the function.
The good news is that Rust doesn't need to know that information.
It just needs to know how the lifetimes of parameters and return values are related.
In the return_first_two function, the only valid implementation for a function with this signature is for the return value to reference the same value that the parameter references.
Therefore,
Rust knows that the return value can live as long as the parameter can live. In main,
we could have created list a long time before calling return_first_two and list could live a long time after.
Or, perhaps list is created a long time before, but goes out of scope soon after.
Or, maybe list is created just before, but lives for a long time after.
Or, maybe it doesn't live very long before or after.
All of these scenarios are valid,
and Rust doesn't need to know which one actually happens, only that the return reference has a lifetime tied to the same value
the parameter is tied to. Next, let's look at a function with two parameters that are references.
We're going to write a function that will simulate a sporting event.
The code that we look at isn't going to compile until we introduce some more syntax in a future video -
we're only going to concentrate on the concepts
for now.
The function is called simulate_game and has two parameters,
each of which are string slices.
The first parameter is the name of the home team,
and the second parameter is the name of the away team.
The function returns a string slice that is the name of the team that won the simulated game.
There are many ways we could implement this function.
One poor simulation would be to always have the home team win.
In this case,
we can see that the lifetime of the return string slice would be tied to the same value that the home parameter is tied to.
Similarly,
we could have the away team always win and the lifetime of the return
string slice would be tied to the same value that the away parameter is tied to.
But what if we choose randomly between the two teams using the rand crate?
With this code,
there's no way to tell at compile time
which parameter the return slice will be tied to.
We, as programmers, need to tell Rust how long the return value is guaranteed to be valid for,
so that Rust has enough information to determine whether references will always be valid at compile time.
We can't know that the return reference will be tied to one or other of the parameters.
What we can know is that the returned reference will definitely be valid for the time when both parameters are valid.
This is a generic lifetime that relates the two input reference's lifetimes to the output reference's lifetime.
We need to tell the compiler about this relationship.
In other words,
we're saying that the output reference will live as long as some concrete lifetime that we don't know about at compile time.
But it'll be no greater than the time that both of the parameters are valid. Here's an example main function that calls simulate_game.
We've added an inner scope to make the concrete lifetimes more obvious.
Rust can see that the value in team1 lives from where it's declared to the end of main.
The value in team2 lives from where it's declared until the end of the inner scope.
We don't know at compile time whether team1 or team2 will win.
But we do know that the reference winner binds to will definitely be valid as long as team2 is valid because team2 has the shorter lifetime.
This code is valid and would compile
except for the syntax we haven't talked about yet that we need to add to the simulate_game function.
Now, let's look at a main function that's invalid. In this variation,
team1 is still valid from where it's declared until the end of main, and team2 is still valid from where it's declared to the end of the inner scope.
This code attempts to bind the reference returned by simulate_game to the variable winner,
which is declared in the outer scope.
This code isn't allowed because if team2 wins,
the reference held by winner would be invalid because team2 gets cleaned up.
If team1 wins,
the reference in winner would be valid,
but Rust can't guarantee that team1 will win at compile time.
And the relationship we specify to Rust about the generic lifetimes involved in the simulator_game function say the return reference is only valid for the shorter of the two lifetimes.
For the third example,
let's look at a method defined on a struct.
We're going to write a very simple stemmer -
a program that removes suffixes from words,
which is useful when searching,
for example.
This stemmer will be so simple that each instance supports the removal of only one suffix.
We're going to implement the stemmer as a struct that owns a string of the suffix that it removes.
The stemming part is implemented as a method named stem, defined on the Stemmer struct, that takes a string slice of the word to be stemmed.
The stem method returns a string slice.
If the word ends in the stemmer
suffix, the stem method returns a smaller string slice of the word without the suffix.
If the word doesn't end in the suffix,
the stem method returns the whole word.
Here's a test illustrating the desired functionality.
But again,
this example won't compile until we discuss some syntax in a future video.
For now,
though,
let's look at the lifetimes involved in the stem method.
There are actually two references that are inputs to the stem method:
the immutable reference to self that we need to read
the suffix field and the reference to the word being stemmed.
We need to tell Rust the generic lifetime
the return reference is tied to. From the previous example, we know we have three choices:
the return reference can be associated to the reference to self, the reference to word, or the time that both self and word are valid.
The way we want the stem method to work is that it will always return a slice,
either partial or full, of the word parameter.
This means that the generic lifetime relationship we want to tell Rust about is that the return reference is tied to the same value that word is.
Let's look at some concrete lifetimes in a main function where we'll be calling the stem method to see why this is what we want in a different way.
First, we'll create a word that we want to stem.
Then,
in an inner scope,
we'll create an instance of stemmer with the suffix, ed.
We'll return the stem of the word from the inner scope and bind it to the variable word_stem.
And, finally,
we'll print out the word and its stem.
The lifetime of word goes from where word is declared until the end of main.
The lifetime of the stemmer instance lasts from when it's declared to the end of the inner scope.
In order for this code to be valid,
word_stem needs to live from where it's declared to the end of main.
This code is valid because the relationship we want to add to the stem method says that the return reference is tied to the lifetime of the word parameter.
If the reference returned from the stem method was tied to the self reference, or to both the self reference and the word parameter,
this code wouldn't be allowed because the stemmer
instance
gets cleaned up at the end of the inner scope.
word_stem lives longer than that,
and the compiler would reject
the code as having an invalid reference. In this module,
we defined a generic lifetime as the relationship that concrete lifetimes must have for the code to be valid.
We looked at the return_first_two function that had one generic lifetime.
We looked at the simulate_game function that needs to know that the lifetime of the return reference will be valid when both of the parameter's references are valid.
And we looked at the stem method, where the return reference is related to one of the parameter's references.
In the next module, we're going to take a slight detour to explore generic type parameters and their syntax,
which will set us up for understanding generic lifetime parameter syntax.