I later realized that a semi-conscious goal with the âhypothetical tutorialâ in my previous post was avoiding lifetime parameters until absolutely necessary, because lifetime parameters are kind of weird. I think itâs worth expanding on that issue.
How exactly are lifetime parameters âweirdâ?
- We can receive lifetime parameters, but we canât pass them, unlike all other kinds of parameters.
struct S<X: Debug> { // receiving a type parameter (presumably const parameters will be similar)
x: X
}
fn main() {
let s: S<i32>; // passing a type parameter (presumably const parameters will be similar)
...
}
struct R<'a> { // receiving a lifetime parameter
r: &'a i32
}
fn main() {
let x = 42i32;
let r: R<???>; // no way to pass "the lifetime of x" to R here
...
}
- Lifetime parameters arenât really lifetimes.
Most users either assume or are taught that a lifetime parameter is âthe lifetimeâ of a certain variable, block, scope, borrow, or whatever from the call site. Usually thatâs good enough, but in some cases (like Opposite of &'static ) that quickly breaks down and you have to start thinking of lifetime parameters as a set of borrow checker constraints.
This is obviously closely related to #1, since part of the reason you canât âpassâ a lifetime parameter is because we donât have a syntax for writing âborrow checker constraintsâ in Rust source code.
As far as I know, type parameters are types and const parameters will be const values.
âDirectâ Solutions / Strawman Syntax Ideas
A lot of the elision ideas already discussed attack this problem by simply making lifetime parameters not appear in the source code at all, which does help a lot, but since we want a comprehensive rethink and not just an incremental improvement we should talk about more âdirectâ solutions as well.
I think direct solutions would generally fall into one of two categories. Warning: lots of strawman syntax here.
- Make lifetime parameters act like parameters: You can pass them and they are the lifetime of something.
struct R<'a> {
ref: &'a i32
}
fn main() {
let x = 1;
{
let y = 2;
let r1 = R<lifetimeof y>{ ref: &x }; // OK, x is alive at least as long as y is
let r2 = R<lifetimeof x>{ ref: &y }; // ERROR, y does not live as long as x does
}
}
- Replace âlifetime parametersâ with some non-parameter syntax(es?).
fn foo(a: &str, b: &str) -> &'a str {
// where 'a is the constraint "has the same lifetime as a"
...
}
#[lifetime_constraint(a, same_as(b), different_from(c))]
struct Foo<'> {
a: &i32,
b: &i32,
c: &str,
}
This overlaps a lot with making lifetime parameters ânot appear in the source codeâ, but I think itâs important for the mental model that we decide whether it should be conceptually correct to view this sort of change as âhiding lifetime parametersâ or âexpressing lifetime constraintsâ. We all seem to support being able to return &'arg1 str
, but is that 'arg1
a âconstraintâ in and of itself or is it merely sugar for fn foo<'a>(arg1: &'a str) -> &'a str
?
Conclusion
My current feeling is that we should lean towards making âlifetime parametersâ act like lifetime parameters, and treat all ânon-parameter syntaxesâ as syntactic sugar that can always be expanded to use explicit lifetimes at both the âcall siteâ and the âreceiverâ.
The reason I lean in that direction is that âexplicitly passing lifetimesâ would be a huge win for teaching Rust, and to a lesser extent for debugging lifetime issues and helping tricky lifetime code be more self-documenting.
P.S. Regarding closures, I do think âjust documentingâ the current tricks is sufficient, and capture lists wouldnât really pull their weight (unlike in C++).