1
00:00:00,090 --> 00:00:08,450
Welcome the module 4, An exploration of generic lifetimes. In this module, we'll discuss what a generic lifetime is,
2
00:00:08,770 --> 00:00:12,650
as opposed to the concrete lifetimes we discussed in modules 2 and 3.
3
00:00:13,640 --> 00:00:18,300
We'll go through three examples that return references and illustrate generic lifetimes:
4
00:00:18,500 --> 00:00:19,940
a function with one parameter
5
00:00:19,970 --> 00:00:20,870
that's a reference,
6
00:00:21,050 --> 00:00:22,299
a function with two parameters
7
00:00:22,300 --> 00:00:24,619
that are references, and a method with a parameter
8
00:00:24,620 --> 00:00:25,450
that's a reference.
9
00:00:26,940 --> 00:00:30,350
Let's start with defining what we mean by "generic lifetime."
10
00:00:31,840 --> 00:00:40,820
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.
11
00:00:41,840 --> 00:00:45,450
Generic lifetimes can exist in the definitions of functions,
12
00:00:45,570 --> 00:00:49,410
methods, structs, enums, and traits.
13
00:00:49,920 --> 00:00:50,840
In this video,
14
00:00:50,850 --> 00:00:58,220
we're going to cover generic lifetimes in functions and methods.
15
00:00:58,420 --> 00:01:05,950
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.
16
00:01:07,440 --> 00:01:07,709
Now,
17
00:01:07,710 --> 00:01:10,750
let's look at some examples involving generic lifetimes,
18
00:01:10,960 --> 00:01:13,700
starting with a function with one reference as a parameter,
19
00:01:13,890 --> 00:01:15,050
and that returns a reference.
20
00:01:16,100 --> 00:01:20,410
We're actually going to look again at the example from the module on concrete lifetimes,
21
00:01:20,420 --> 00:01:22,350
with the return_first_two function.
22
00:01:22,840 --> 00:01:33,850
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.
23
00:01:34,840 --> 00:01:43,250
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.
24
00:01:44,240 --> 00:01:47,790
The orange lines actually represented a generic lifetime,
25
00:01:47,920 --> 00:01:50,450
but we glossed over that detail in the earlier video.
26
00:01:51,440 --> 00:01:52,430
In that video,
27
00:01:52,440 --> 00:01:53,969
we were able to look at main,
28
00:01:53,970 --> 00:01:55,770
where return_first_two was called,
29
00:01:56,030 --> 00:01:58,220
and see that the concrete lifetimes were valid.
30
00:01:59,240 --> 00:01:59,950
However,
31
00:02:00,050 --> 00:02:08,370
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,
32
00:02:08,380 --> 00:02:10,750
not only the calls currently available to the compiler.
33
00:02:12,240 --> 00:02:13,080
For instance,
34
00:02:13,110 --> 00:02:15,829
if the return_first_two function is part of the public
35
00:02:15,830 --> 00:02:18,010
API of a crate that we publish on crates.io,
36
00:02:18,020 --> 00:02:22,360
Rust has no way of knowing all of the concrete ways people downloading the crate in the future
37
00:02:22,370 --> 00:02:23,350
will call the function.
38
00:02:23,990 --> 00:02:26,940
The good news is that Rust doesn't need to know that information.
39
00:02:27,190 --> 00:02:34,810
It just needs to know how the lifetimes of parameters and return values are related.
40
00:02:34,870 --> 00:02:42,150
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.
41
00:02:42,740 --> 00:02:43,370
Therefore,
42
00:02:43,380 --> 00:02:48,820
Rust knows that the return value can live as long as the parameter can live. In main,
43
00:02:48,830 --> 00:02:55,220
we could have created list a long time before calling return_first_two and list could live a long time after.
44
00:02:56,210 --> 00:03:00,450
Or, perhaps list is created a long time before, but goes out of scope soon after.
45
00:03:01,440 --> 00:03:05,150
Or, maybe list is created just before, but lives for a long time after.
46
00:03:06,140 --> 00:03:09,250
Or, maybe it doesn't live very long before or after.
47
00:03:10,340 --> 00:03:12,300
All of these scenarios are valid,
48
00:03:12,310 --> 00:03:18,460
and Rust doesn't need to know which one actually happens, only that the return reference has a lifetime tied to the same value
49
00:03:18,470 --> 00:03:24,950
the parameter is tied to. Next, let's look at a function with two parameters that are references.
50
00:03:25,990 --> 00:03:28,850
We're going to write a function that will simulate a sporting event.
51
00:03:29,440 --> 00:03:34,410
The code that we look at isn't going to compile until we introduce some more syntax in a future video -
52
00:03:34,420 --> 00:03:36,159
we're only going to concentrate on the concepts
53
00:03:36,160 --> 00:03:36,650
for now.
54
00:03:37,640 --> 00:03:41,020
The function is called simulate_game and has two parameters,
55
00:03:41,330 --> 00:03:43,150
each of which are string slices.
56
00:03:43,640 --> 00:03:46,200
The first parameter is the name of the home team,
57
00:03:46,210 --> 00:03:48,710
and the second parameter is the name of the away team.
58
00:03:49,700 --> 00:03:54,550
The function returns a string slice that is the name of the team that won the simulated game.
59
00:03:55,440 --> 00:03:57,800
There are many ways we could implement this function.
60
00:03:57,960 --> 00:04:01,210
One poor simulation would be to always have the home team win.
61
00:04:01,740 --> 00:04:02,630
In this case,
62
00:04:02,640 --> 00:04:08,550
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.
63
00:04:09,940 --> 00:04:10,820
Similarly,
64
00:04:11,030 --> 00:04:14,489
we could have the away team always win and the lifetime of the return
65
00:04:14,490 --> 00:04:18,250
string slice would be tied to the same value that the away parameter is tied to.
66
00:04:19,240 --> 00:04:23,250
But what if we choose randomly between the two teams using the rand crate?
67
00:04:23,740 --> 00:04:24,650
With this code,
68
00:04:24,710 --> 00:04:26,860
there's no way to tell at compile time
69
00:04:26,870 --> 00:04:29,410
which parameter the return slice will be tied to.
70
00:04:30,440 --> 00:04:34,930
We, as programmers, need to tell Rust how long the return value is guaranteed to be valid for,
71
00:04:35,080 --> 00:04:40,150
so that Rust has enough information to determine whether references will always be valid at compile time.
72
00:04:41,150 --> 00:04:45,650
We can't know that the return reference will be tied to one or other of the parameters.
73
00:04:46,140 --> 00:04:52,050
What we can know is that the returned reference will definitely be valid for the time when both parameters are valid.
74
00:04:52,540 --> 00:04:58,950
This is a generic lifetime that relates the two input reference's lifetimes to the output reference's lifetime.
75
00:04:59,440 --> 00:05:02,150
We need to tell the compiler about this relationship.
76
00:05:03,210 --> 00:05:04,250
In other words,
77
00:05:04,450 --> 00:05:10,620
we're saying that the output reference will live as long as some concrete lifetime that we don't know about at compile time.
78
00:05:10,860 --> 00:05:19,250
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.
79
00:05:19,740 --> 00:05:23,810
We've added an inner scope to make the concrete lifetimes more obvious.
80
00:05:24,340 --> 00:05:29,150
Rust can see that the value in team1 lives from where it's declared to the end of main.
81
00:05:29,640 --> 00:05:34,050
The value in team2 lives from where it's declared until the end of the inner scope.
82
00:05:34,740 --> 00:05:38,500
We don't know at compile time whether team1 or team2 will win.
83
00:05:38,510 --> 00:05:45,950
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.
84
00:05:47,040 --> 00:05:48,949
This code is valid and would compile
85
00:05:48,950 --> 00:05:53,350
except for the syntax we haven't talked about yet that we need to add to the simulate_game function.
86
00:05:54,840 --> 00:05:59,240
Now, let's look at a main function that's invalid. In this variation,
87
00:05:59,350 --> 00:06:06,750
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.
88
00:06:07,740 --> 00:06:12,590
This code attempts to bind the reference returned by simulate_game to the variable winner,
89
00:06:12,770 --> 00:06:14,350
which is declared in the outer scope.
90
00:06:15,970 --> 00:06:19,180
This code isn't allowed because if team2 wins,
91
00:06:19,210 --> 00:06:23,350
the reference held by winner would be invalid because team2 gets cleaned up.
92
00:06:24,340 --> 00:06:25,850
If team1 wins,
93
00:06:25,920 --> 00:06:28,040
the reference in winner would be valid,
94
00:06:28,290 --> 00:06:31,630
but Rust can't guarantee that team1 will win at compile time.
95
00:06:31,830 --> 00:06:40,680
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.
96
00:06:42,240 --> 00:06:43,590
For the third example,
97
00:06:43,720 --> 00:06:46,050
let's look at a method defined on a struct.
98
00:06:47,640 --> 00:06:49,770
We're going to write a very simple stemmer -
99
00:06:50,000 --> 00:06:52,670
a program that removes suffixes from words,
100
00:06:52,910 --> 00:06:54,129
which is useful when searching,
101
00:06:54,130 --> 00:06:54,810
for example.
102
00:06:55,280 --> 00:07:00,950
This stemmer will be so simple that each instance supports the removal of only one suffix.
103
00:07:02,040 --> 00:07:07,150
We're going to implement the stemmer as a struct that owns a string of the suffix that it removes.
104
00:07:07,640 --> 00:07:15,820
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.
105
00:07:16,310 --> 00:07:18,680
The stem method returns a string slice.
106
00:07:19,180 --> 00:07:20,969
If the word ends in the stemmer
107
00:07:20,970 --> 00:07:26,150
suffix, the stem method returns a smaller string slice of the word without the suffix.
108
00:07:26,640 --> 00:07:28,850
If the word doesn't end in the suffix,
109
00:07:29,030 --> 00:07:30,950
the stem method returns the whole word.
110
00:07:31,940 --> 00:07:34,840
Here's a test illustrating the desired functionality.
111
00:07:34,980 --> 00:07:35,780
But again,
112
00:07:35,980 --> 00:07:40,050
this example won't compile until we discuss some syntax in a future video.
113
00:07:40,540 --> 00:07:40,949
For now,
114
00:07:40,950 --> 00:07:41,500
though,
115
00:07:41,600 --> 00:07:44,650
let's look at the lifetimes involved in the stem method.
116
00:07:46,100 --> 00:07:49,440
There are actually two references that are inputs to the stem method:
117
00:07:49,580 --> 00:07:52,139
the immutable reference to self that we need to read
118
00:07:52,140 --> 00:07:55,250
the suffix field and the reference to the word being stemmed.
119
00:07:56,240 --> 00:07:58,610
We need to tell Rust the generic lifetime
120
00:07:58,620 --> 00:08:05,080
the return reference is tied to. From the previous example, we know we have three choices:
121
00:08:05,090 --> 00:08:12,550
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.
122
00:08:14,070 --> 00:08:18,470
The way we want the stem method to work is that it will always return a slice,
123
00:08:18,640 --> 00:08:21,350
either partial or full, of the word parameter.
124
00:08:22,840 --> 00:08:30,650
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.
125
00:08:31,640 --> 00:08:38,850
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.
126
00:08:39,940 --> 00:08:42,550
First, we'll create a word that we want to stem.
127
00:08:43,080 --> 00:08:43,770
Then,
128
00:08:43,870 --> 00:08:44,950
in an inner scope,
129
00:08:44,960 --> 00:08:48,179
we'll create an instance of stemmer with the suffix, ed.
130
00:08:48,180 --> 00:08:54,750
We'll return the stem of the word from the inner scope and bind it to the variable word_stem.
131
00:08:55,240 --> 00:08:56,010
And, finally,
132
00:08:56,050 --> 00:08:58,150
we'll print out the word and its stem.
133
00:08:59,170 --> 00:09:03,550
The lifetime of word goes from where word is declared until the end of main.
134
00:09:04,060 --> 00:09:09,450
The lifetime of the stemmer instance lasts from when it's declared to the end of the inner scope.
135
00:09:10,480 --> 00:09:12,430
In order for this code to be valid,
136
00:09:12,760 --> 00:09:16,550
word_stem needs to live from where it's declared to the end of main.
137
00:09:18,040 --> 00:09:25,950
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.
138
00:09:26,940 --> 00:09:33,610
If the reference returned from the stem method was tied to the self reference, or to both the self reference and the word parameter,
139
00:09:33,620 --> 00:09:35,829
this code wouldn't be allowed because the stemmer
140
00:09:35,830 --> 00:09:36,389
instance
141
00:09:36,390 --> 00:09:39,169
gets cleaned up at the end of the inner scope.
142
00:09:39,170 --> 00:09:40,660
word_stem lives longer than that,
143
00:09:40,880 --> 00:09:42,249
and the compiler would reject
144
00:09:42,250 --> 00:09:46,950
the code as having an invalid reference. In this module,
145
00:09:46,960 --> 00:09:53,250
we defined a generic lifetime as the relationship that concrete lifetimes must have for the code to be valid.
146
00:09:54,240 --> 00:09:58,550
We looked at the return_first_two function that had one generic lifetime.
147
00:09:59,540 --> 00:10:07,450
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.
148
00:10:08,440 --> 00:10:16,330
And we looked at the stem method, where the return reference is related to one of the parameter's references.
149
00:10:16,380 --> 00:10:21,220
In the next module, we're going to take a slight detour to explore generic type parameters and their syntax,
150
00:10:21,530 --> 00:10:25,250
which will set us up for understanding generic lifetime parameter syntax.