We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership III: Lifetimes, Explicitly step 22 of 22
Generics, traits and lifetimes in one signature
Lifetime parameters and type parameters live in the same angle brackets. Before you can read any real library signature you have to be comfortable seeing them together — including seeing when one of them should not be there at all.
pub fn best_by<T, K>(items: &[T], key: K) -> Option<&T> where K: Fn(&T) -> i64;
pub fn describe<T: Display>(items: &[String], tag: T) -> (String, Option<&str>);
pub fn store<'a, 'b: 'a, T>(long: &'b T, out: &mut Vec<&'a T>);
-
best_byreturns the element with the largest key, first wins on ties,Nonefor an empty slice. -
describereturns(format!("{tag}#{n}"), items.first())wherenis the number of items — the summary is owned, the second half is borrowed. -
storepusheslongintoout.
Why only one of the three names a lifetime
This is the part worth internalising. Two of these functions are generic and return references, and neither needs an explicit lifetime:
-
best_by(items: &[T], key: K)—key: Kis a type parameter with no lifetime position in it, soitemsis the only lifetime-carrying parameter and elision rule 2 gives the returnOption<&T>that lifetime. -
describe(items: &[String], tag: T)— same shape.
Write them with <'a> anyway and clippy::needless_lifetimes fails your
submission. The rule is not “generic code needs lifetime annotations”; the rule
is exactly what it always was — name a lifetime when elision cannot work out
what you meant, and not before.
store is the one that cannot elide, and it is the reason this problem exists.
'b: 'a — the bound that reads backwards
out holds references valid for 'a. long is valid for 'b. Pushing one
into the other is only sound if 'b covers at least all of 'a, and rustc
will not take that on faith:
error: lifetime may not live long enough
... argument requires that `'b` must outlive `'a`
No error code — this is one of the bare diagnostics. The fix is to say it in the signature:
pub fn store<'a, 'b: 'a, T>(long: &'b T, out: &mut Vec<&'a T>)
'b: 'a reads “'b outlives 'a“ — 'b lasts at least as long as 'a,
so anywhere a &'a is needed a &'b will do. Most people read the colon
backwards the first ten times. Draw the containment picture instead: 'b‘s
region contains 'a‘s region. Subtyping follows: &'b T is a subtype of
&'a T, so it coerces where the shorter one is expected.
The generated driver does exactly this — out is a Vec<&i64> living in a
nested block, while values lives for the whole function. 'b is strictly
bigger than 'a, which is the ordinary case, and without the bound the ordinary
case does not compile.
Two more things you get for free and should recognise:
-
Implied bounds.
out: &mut Vec<&'a T>requiresT: 'a, and you never have to write it: rustc infers lifetime bounds implied by the parameter types. (rustc’s allow-by-defaultexplicit_outlives_requirementslint exists to tell you to delete such bounds when you do write them.) Note the asymmetry — only lifetime bounds are implied. Trait bounds never are. -
Ordering. Lifetimes come first in the parameter list.
<T, 'a>is a syntax error, not a style preference.
The pins
pub type KeyFn = fn(&i64) -> i64;
const _: for<'a> fn(&'a [i64], KeyFn) -> Option<&'a i64> = best_by;
const _: for<'a> fn(&'a [String], u8) -> (String, Option<&'a str>) = describe;
They instantiate the generics at concrete types and check the borrowing shape
survives. (The KeyFn alias is not decoration —
for<'a> fn(&'a [i64], fn(&i64) -> i64) -> Option<&'a i64> written out in full
trips clippy::type_complexity, which is warn-by-default and therefore fatal
here. Factoring a type out into an alias is the standard answer to that lint.)
There is deliberately no pin on store: an outlives relation between two
binders is not expressible in a for<'a, 'b> fn(..) type, and a pin without it
would reject the correct answer. store is graded by whether its own body
compiles.
The tie trap, again
items.iter().max_by_key(&key) returns the last maximum. Three of the tests
have ties. Iterator::reduce with a strict > keeps the first, and reads
well: “fold the elements pairwise, keeping the better one”.
A footnote for later: K: Fn(&T) -> i64 is quietly higher-ranked — the
Fn-trait sugar expands to for<'r> Fn(&'r T) -> i64, so key must work for
a reference of any lifetime. You did not have to write that, and most of the
time you never will.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.