Some of the suggestions here I find unconvincing in isolation because they seem to only reduce the number of keystrokes without either simplifying the mental model or making the code clearly more readable. I think we need clearer motivation in terms of the overall mental model we want to implement before spending much more time on the syntax bikeshed. But I'm going to throw out some other thoughts before getting back to that point.
To me the clearest example of this is the single-reference struct case. On its own, I just don't see the benefit here.
struct Foo<'a> {
t: &'a i32
}
struct Foo<'> {
t: &i32
}
The second version doesn't prevent the user from having to think about the implications of a type having lifetime parameters/constraints, nor does it make it any more obvious what those implications are. I also can't think of any user errors that could be given a better error messages with the latter syntax. It seems like the only advantage to this sugar is (slightly) reducing the number of keystrokes. If this syntax somehow magically scaled to cases with multiple lifetimes I'd probably like it more, but obviously it can't do that. I would probably be against doing this were it not for the argument I bring up later in this post.
In contrast, I am extremely strongly in favor of inferring T: 'a
constraints. These are technical details the programmer almost never cares about because it's always either objectively correct or objectively incorrect to add such a constraint. The decision to include or exclude such a constraint never represents a meaningful decision about a type's semantics and public API contractl; it only indicates the presence or absence of a compiler error. Being able to omit such constraints also allows the actually meaningful information about what lifetimes exist and who has what lifetime to stand out more.
Function signatures are more interesting, because I actually agree there's a problem here, but I disagree with the proposed solution. It is unfortunate that fn foo(x: &i32, ...) -> Foo<the lifetime of x>
can only be expressed today by adding an explicit lifetime parameter in three separate places. The examples at the beginning of this thread appear to be proposing we reduce that to two separate places. But if we're going to improve this at all, why leave it at two? Why not just one, i.e. fn foo(x: &i32, ...) -> Foo<'x>
? That totally eliminates the distracting redundancy and is more consistent with the cases where Foo<'>
works.
Tangent: Someone suggested Foo<'x> is a little too magical and we should have a different syntax. For totally unrelated reasons, we're going to want typeof
in the language someday. Assuming epochs eventually allow us to add new keywords, we could do lifetimeof
, which would give us fn foo(x: &i32) -> Foo<lifetimeof(x)>
. I have no idea whether I actually want this, but it's a thought I had.
Now for my serious suggestion: I would like to be able to "explain lifetimes" to a Rust user like this:
In function signatures, you usually don't have to worry about lifetimes because Rust will infer them for you.
// x has a lifetime, but there's no reason to mention it
fn foo(x: &i32) { ... }
In structs, you usually only care about distinguishing structs which do not contain any references/lifetimes at all from structs which do contain at least one reference/lifetime.
// The return value of foo() is NOT constrained by the lifetime of x
struct Foo { ... }
fn foo(x: &i32) -> Foo { ... }
// The return value of bar() IS constrained by the lifetime of x
struct Bar<'> { ... }
fn bar(x: &i32) -> Bar<'> { ... }
You only need to explicitly name lifetimes when there are multiple references involved and you need a lifetime constraint to apply to some but not all of those references. For example:
// Foo must not outlive x or s
fn foo(x: &i32, s: &str) -> Foo<'> { ... }
// Foo must not outlive s, but it is allowed to outlive x
fn foo(x: &i32, s: &str) -> Foo<'s> { ... }
This usually becomes useful when writing generic code or libraries where your clients might pass arguments of varying types and lifetimes (in particular, some 'static arguments and some non-'static).
Although I believe struct Foo<'>
syntax has little benefit on its own, I also believe not having that syntax would make this sort of explanation more cluttered and less self-evident, and that's probably good enough motivation.
I have very little experience with non-toy Rust code, so I've probably gotten something horribly wrong in this hypothetical future tutorial, but I think our goal should be figuring out what we want this future tutorial to look like (emphasis on tutorial, not exhaustive reference). Or equivalently, what the overall mental model for lifetimes should become (as opposed to whatever it is now). I don't think we're at the point where it makes sense to be debating whether Foo<&> is more intuitive than Foo<'> in isolation from the rest of lifetime syntax/elision.
P.S. Except that I would like to echo the sentiment that Foo&
looks way too much like "Foo reference".