Lifetime notation redux
15 January 2013
In a previous post I outlined some of the options for updating our lifetime syntax. I want to revist those examples after having given the matter more thought, and also after some discussions in the comments and on IRC.
My newest proposal is that we use <>
to designate lifetime
parameters on types and we lean on semantic analysis (the resolve
pass, more precisely) to handle the ambiguity between a lifetime name
and a type name. Before I always wanted to have the distinction
between lifetimes and types be made in the parser itself, but I think
this is untenable. This proposal has the advantage that the most
common cases are still written as they are today.
Here is the example from the previous post in my proposed notation:
struct StringReader<&self> {
// ^~~~~ Lifetime parameter designated with &
value: &self/str,
// ^~~~~~~~~ Same as today.
count: uint
}
impl StringReader {
fn new(value: &self/str) -> StringReader<&self> {
// ^~~~~
// Interpreted as a lifetime reference due to
// the declaration of StringReader, which states
// that first parameter is a lifetime.
StringReader { value: value, count: 0 }
}
}
fn value(s: &v/StringReader<&v>) -> &v/str {
// ^~~~~~~~~~~~~~~~~~~ ^~~~~~
// As today, lifetime names that appear in a function declaration
// do not have to be declared anywhere and are implicitly scoped
// to the containing function declaration.
return s.value;
}
fn remaining(s: &StringReader<&> -> uint {
// ^~~~~~~~~~~~~~~~
// A bare & in a fn decl means "use a fresh name",
// so this is equivalent to &x/StringReader<&y>.
// This may be the right thing, see Option 2 below.
return s.value.len() - s.count;
}
What follows are miscellaneous notes and thoughts. There are a few options that could be tweaked, which I have noted.
Considerations
The only way I have found to distinguish lifetime names purely in the
parser that is also visually appealing is to use braces to designate
lifetimes (options 7 and 8 in my previous post). As a reminder,
the impl of StringReader
would look like:
impl StringReader {
fn new(value: &{self} str) -> StringReader{self} {
StringReader { value: value, count: 0 }
}
}
The major problem here is that, as bstrie pointed out on IRC, it’s
ambiguous: the {self}
which appears in the return type could be
interpreted as the function body. His proposed fix was to use
whitespace sensitivity, so that StringReader{self}
and StringReader {self}
are parsed differently, but whitespace sensitivity is
something we have always tried to avoid.
I personally find it appealing to use <>
both for lifetime and type
parameters, because I think it gives the right intution. A
lifetime-parameterized declaration is just like a type-parameted
declaration with regard to how it works in the type system.
OPTION 1: I opted to include &
in the lifetime parameters to a
type for consistency (this way, a lifetime name is always preceded by
&
). However, they are not strictly necessary and they are visually
heavy. We could remove them, which would mean you have
&v/StringReader<v>
and StringReader<self>
and not
&v/StringReader<&v>
and StringReader<&self>
. However, the default
would still have to be written &
, so you’d still have
StringReader<&>
.
The default lifetime &
In this proposal, the “default lifetime” &
would only be usable
inside a function declaration or function body. In a function
declaration, it means “use a fresh lifetime. In the function body it
means “use inference”.
OPTION 2: It would be possible to make &
a little smarter, as it
is today. Today it means “use a fresh name unless &
appears on a
nested type, then use the lifetime you are nested within”. If we took
that interpretation, then &StringReader<&>
would be equivalent to
&x/StringReader<&x>
and not &x/StringReader<&y>
. This is more
likely to be what the user wanted, though I don’t think it makes much
difference in practice. I’d probably just want to experiment a bit
here: start with the simpler version, as I proposed here, and then see
how many type errors we get
OPTION 3: We could also allow users to leave off the <>
if the
only parameter is a lifetime parameter, in which case it would be
equivalent to <&>
. This means that you could write &StringReader
instead of &StringReader<&>
.
OPTION 4: The one place that I opted to eschew explicit declarations
is on functions. If we wanted, we could always require that all named
lifetimes be declared, which would mean that the function value()
above would be written:
fn value<&v>(s: &v/StringReader<v>) -> &v/str {
return s.value;
}
I can’t decide about this option. It strikes me as a reasonably simple story, which appeals to me, but it’s also fairly heavyweight.
How complex can it get?
UPDATE: Per bstrie’s request, here is an example of a type that uses both lifetime and type parameters with trait bounds:
struct Foo<&self, T: Reader+Eq> {
value: &self/T,
count: uint
}
fn operate<R: Reader+Eq>(f: Foo<&, R>)
{
...
}