Eliding higher rank lifetimes in trait bounds


#1
// "Function" trait syntax:
F: Fn(&T) -> T
// Expands to:
for<'a> F: Fn<&'a T, Output = U>

Function traits have special sugar to give them an ergonomic and familiar shape, while under the hood they are treated consistently with other traits. This sugar currently combines two steps:

  1. It elides higher ranked lifetime parameters; if a lifetime parameter is excluded from the function, it is treated as being higher rank.
  2. It converts from the form Fn(Args..) -> Return to a more regular form Fn<Args..., Output = Return>.

I think it would make sense that the ellision step of this be applied to all trait bounds. That is:

trait Foo<'a> { }

F: Foo
// is equivalent to
for<'a> F: Foo<'a>

trait Bar<T> { }
B: Bar<&i32>
// is equivalent to
for<'a> B: Bar<&'a i32>

// and so on

Reasons:

  1. I think that this is very commonly what you want.
  2. This is the most general bound and the most restrictive bound. This makes it a sane default.
  3. If you meant something else that could be valid code, you already have a lifetime in scope, all you have to do is apply it, making the suggested fix easy for rustc to provide.
  4. This feature is very useful, but its syntax is arcane and not obvious. Users who need this will be likely to ‘flail’ at first, trying to invalidly add lifetime variables to their impl and so on. This could lead them ‘down the garden path’ as the compiler, more and more confused about what they’re trying to express, may make bad suggestions to them about how to annotate their code.
  5. It can make the API of libraries doing complex type tricks easier to read. I have a function in a library I’m working on with a constraint for<'x> T: Trait<Struct<'x, U, Self>>. This is a very complicated constraint, and reducing it to T: Trait<Struct<U, Self>> could help somewhat.

Opinions? :smile:


#2

Isn’t the syntax F: for<'a> Fn<&'a T, Output = U>?


#3

Both syntaxes are valid in where clauses; for before the variable is hypothetically more general (if there were higher kinded variables, you could do e.g. for<'a> T<'a>: Foo<'a>), so I think it of it as the ‘canonical’ form and I used it here, but they mean the same thing.

EDIT: the generality is not actually hypothetical, you can write today where for<'a> &'a T: Trait.


#4

Does where for<'a> T: Trait<'a> work?


#5

Yes, I think @eddyb explained once that T: for<'a> Trait<'a> should be thought of as just sugar for for<'a> T: Trait<'a>.

fn foo<F>(f: F) where for<'a> F: Fn(&'a str) {
    f("hello, world");
}

https://is.gd/wjEpRy


#6

I am of two minds.

On the one hand, I do want to make it less arduous to have to type 'a all the time, especially for lifetimes only used once. On the other hand, I am wary of “hiding” where lifetimes flow – this can make for some very confusing errors!

For example, although it is by design (and in fact I insisted on it), I (somewhat) regret that the following works in elision:

struct Ref<'a, T>;
...
impl Foo {
    fn foo(&self) -> Ref<i32> { ... }
}

This is because, when I read that signature, I can’t see at a glance that self will remain borrowed so long as the return value is in use (in contrast to, e.g., fn foo(&self) -> &i32, where I find the result quite obvious).

But also, I am not sure about this:

I feel like I’ve almost never wanted this. If a trait has a lifetime parameter – which itself is rare – it’s usually tied to some elements in the Self type and not something “free-floating”, at least in my experience. This can be answered with data, I suppose.

This does worry me, of course, as does your point 5. :slight_smile:


#7

I can see why - I think to be honest this is more of a weakness of the syntax of lifetime parameters, because you can’t see that anything has been elided. I find fn foo(&self) -> &str, for example, much less opaque because I can see where the missing lifetime would go. (The fact that it shares the sigil with self also helps).

But this is less likely to be an issue with this ellision, I feel, because it doesn’t bind the elided lifetime to any lifetime in particular.

The use case in which I’m seeing this pop a lot is a trait defined like this:

trait Foo<T: Bar> {
    fn foo(&mut self, &T);
}

trait Bar { ... }

My library provides implementations of Bar, you provide implementations of Foo, which you pass to some function, and then I manipulate your Foo with a Bar.

Several of my Bar implementations have lifetime parameters (some more than one), so I end up with functions like:

fn baz<T>(foo: T) where T: for<'a, 'b> Foo<BarImpl<'a, 'b>>

(The real signatures have even more elements). The reason I explain this is that the trait itself isn’t parameterized by a lifetime (I agree that this is very rare), but that its parameterized by a type carrying a lifetime.

Of course what I’d really like here is better impl traits, so I could instead define it as:

fn baz<T>(foo: T) where T: Foo<impl Bar>