Hi all,
(My first post here.)
First, congrats for what you are trying to achieve with Rust. I arrived
very late to the party, and so I am not sure that what I will say can be
of use. But as I understand, lifetimes is one of those things that are
not completely solidified, and anyway, better late than never.
Looking at some code, expressing lifetimes of borrowed references is one
of those things that is somewhat bewildering, making the code somewhat
noisy. I think it can be improved through a better choice of defaults
and a slight change in explicit lifetimes.
The main thing I think is "wrong" is the defaults for function
parameters. From the tutorial: "the compiler simply creates a fresh name
for the lifetime automatically: that is, the lifetime name is guaranteed
to refer to a distinct lifetime from the lifetimes of all other parameters."
This default is not very useful. For example, it is wrong basically
everytime we want to return a borrowed pointer (unless, for a global
variable?). The more common case is returning something with the same
lifetime as some parameter. In many cases we don't need to distinguish
parameters, and specify which we are returning, in others we want to
split parameters in two equivalence classes, the one from which we are
returning, and everything else.
When type parameters are involved, a return type typically says where
the result comes from most of the times. Again, the default should be
different. E.g., when we are returning a borrowed reference to a "key"
of type parameter K, the default should be "other things also of type K
in parameters".
Finally, with better defaults, in the remaining cases where we need to
explicitly express lifetimes, having to "invent" identifiers is a
nuisance. Also the space which must be used between the lifetime
identifier and the type is too distracting and makes it cumbersome (for
humans) to "parse" the type of some identifier.
I have been thinking about this and have the following proposal. (Of
course there may be inumerous things that must have escaped me, but here
it goes anyway.)
---
Regarding types for borrowed references in function parameters or
result, and type parameters:
1) each type-parameter has an associated implicit lifetime, which by
default is different from the lifetimes of other type-parameters or
normal function parameter types, but it can be qualified in each
declaration or use with an explicit lifetime;
2) function parameter types or return type that are not type parameters
have all the same implicit lifetime by default, but they can be
qualified explicitly with some lifetime.
3) explicit lifetimes are written identifier' instead of 'identifier; a
null identifier is allowed, as in &'T, to qualify a reference &T with
lifetime '.
---
(Another useful possibility would be allowing several ' at the end,
allowing e.g., &T, &'T, &''T as borrowed references to the same type but
with different lifetimes. In practice, a single ' plus 1) and 2) will
cover "99%" of cases. We could even get rid of using identifiers, using
only several '. But this is not relevant for the main proposal.)
1) and 2) are about defaults for implicit lifetimes, which currently are
"fresh lifetime for each parameter", and 3) is about simplifying the
remaining cases of explicit expression. The motivation for 3) is to
remove both the need of a space separating lifetime from type and also
the need to "invent" lifetime identifiers. Rewriting examples from the
tutorial and elsewhere, under this proposal, would make code more
legible and standard, reducing a lot the need for explicit lifetime
expression. Things would "just work" as wanted most times.
--------------------------------------------------------------------------------
From the Rust Borrowed Pointers Tutorial
---
fn get_x<'r>(p: &'r Point) -> &'r float { &p.x }
becomes
fn get_x(p: &Point) -> &float { &p.x }
---
fn select<'r, T>(shape: &'r Shape, threshold: float,
a: &'r T, b: &'r T) -> &'r T {
if compute_area(shape) > threshold {a} else {b}
}
becomes
fn select<'T>(shape: &'Shape, threshold: float,
a: &'T, b: &'T) -> &'T {
if compute_area(shape) > threshold {a} else {b}
}
(not very useful case)
---
fn select<'r, T>(shape: &Shape, threshold: float,
a: &'r T, b: &'r T) -> &'r T {
if compute_area(shape) > threshold {a} else {b}
}
becomes
fn select<T>(shape: &Shape, threshold: float,
a: &T, b: &T) -> &T {
if compute_area(shape) > threshold {a} else {b}
}
--------------------------------------------------------------------------------
Other examples:
---
struct Iterator<'lt, T> {
source: &'lt [T],
index: uint
}
fn has_next<'a, 'b, T>(iter: &'a Iterator<'b, T>) -> bool {
iter.index + 1 < iter.source.len()
}
fn next<'a, 'b, T>(iter: &'a mut Iterator<'b, T>) -> Option<&'b T> {
iter.index += 1;
if iter.index < iter.source.len() {
Some(&iter.source[iter.index])
} else {
None
}
}
becomes:
struct Iterator<'T> {
source: &'[T],
index: uint
}
fn has_next<T>(iter: &Iterator<T>) -> bool {
iter.index + 1 < iter.source.len()
}
fn next<T>(iter: &mut Iterator<T>) -> Option<&T> {
iter.index += 1;
if iter.index < iter.source.len() {
Some(&iter.source[iter.index])
} else {
None
}
}
---
impl<'b, T> Iterator<'b, T> {
fn has_next<T>(&self) { /* same as before */ }
fn next<T>(&mut self) -> Option<&'b T> { /* same as before */ }
}
becomes
impl<T> Iterator<T> {
fn has_next<T>(&self) { /* same as before */ }
fn next<T>(&mut self) -> Option<&T> { /* same as before */ }
}
---
Regards,
Paulo
_______________________________________________
Rust-dev mailing list
Rust-dev@mozilla.org
https://mail.mozilla.org/listinfo/rust-dev