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++).