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

Reply via email to