-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Higher-ranked trait bounds #387
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,283 @@ | ||
- Start Date: 2014-10-10 | ||
- RFC PR: (leave this empty) | ||
- Rust Issue: (leave this empty) | ||
|
||
# Summary | ||
|
||
- Add the ability to have trait bounds that are polymorphic over lifetimes. | ||
|
||
# Motivation | ||
|
||
Currently, closure types can be polymorphic over lifetimes. But | ||
closure types are deprecated in favor of traits and object types as | ||
part of RFC #44 (unboxed closures). We need to close the gap. The | ||
canonical example of where you want this is if you would like a | ||
closure that accepts a reference with any lifetime. For example, | ||
today you might write: | ||
|
||
```rust | ||
fn with(callback: |&Data|) { | ||
let data = Data { ... }; | ||
callback(&data) | ||
} | ||
``` | ||
|
||
If we try to write this using unboxed closures today, we have a problem: | ||
|
||
``` | ||
fn with<'a, T>(callback: T) | ||
where T : FnMut(&'a Data) | ||
{ | ||
let data = Data { ... }; | ||
callback(&data) | ||
} | ||
|
||
// Note that the `()` syntax is shorthand for the following: | ||
fn with<'a, T>(callback: T) | ||
where T : FnMut<(&'a Data,),()> | ||
{ | ||
let data = Data { ... }; | ||
callback(&data) | ||
} | ||
``` | ||
|
||
The problem is that the argument type `&'a Data` must include a | ||
lifetime, and there is no lifetime one could write in the fn sig that | ||
represents "the stack frame of the `with` function". Naturally | ||
we have the same problem if we try to use an `FnMut` object (which is | ||
the closer analog to the original closure example): | ||
|
||
```rust | ||
fn with<'a>(callback: &mut FnMut(&'a Data)) | ||
{ | ||
let data = Data { ... }; | ||
callback(&data) | ||
} | ||
|
||
fn with<'a>(callback: &mut FnMut<(&'a Data,),()>) | ||
{ | ||
let data = Data { ... }; | ||
callback(&data) | ||
} | ||
``` | ||
|
||
Under this proposal, you would be able to write this code as follows: | ||
|
||
``` | ||
// Using the FnMut(&Data) notation, the &Data is | ||
// in fact referencing an implicit bound lifetime, just | ||
// as with closures today. | ||
fn with<T>(callback: T) | ||
where T : FnMut(&Data) | ||
{ | ||
let data = Data { ... }; | ||
callback(&data) | ||
} | ||
|
||
// If you prefer, you can use an explicit name, | ||
// introduced by the `for<'a>` syntax. | ||
fn with<T>(callback: T) | ||
where T : for<'a> FnMut(&'a Data) | ||
{ | ||
let data = Data { ... }; | ||
callback(&data) | ||
} | ||
|
||
// No sugar at all. | ||
fn with<T>(callback: T) | ||
where T : for<'a> FnMut<(&'a Data,),()> | ||
{ | ||
let data = Data { ... }; | ||
callback(&data) | ||
} | ||
``` | ||
|
||
And naturally the object form(s) work as well: | ||
|
||
```rust | ||
// The preferred notation, using `()`, again introduces | ||
// implicit binders for omitted lifetimes: | ||
fn with(callback: &mut FnMut(&Data)) | ||
{ | ||
let data = Data { ... }; | ||
callback(&data) | ||
} | ||
|
||
// Explicit names work too. | ||
fn with(callback: &mut for<'a> FnMut(&'a Data)) | ||
{ | ||
let data = Data { ... }; | ||
callback(&data) | ||
} | ||
|
||
// The fully explicit notation requires an explicit `for`, | ||
// as before, to declare the bound lifetimes. | ||
fn with(callback: &mut for<'a> FnMut<(&'a Data,),()>) | ||
{ | ||
let data = Data { ... }; | ||
callback(&data) | ||
} | ||
``` | ||
|
||
The syntax for `fn` types must be updated as well to use `for`. | ||
|
||
# Detailed design | ||
|
||
## For syntax | ||
|
||
We modify the grammar for a trait reference to include | ||
|
||
for<lifetimes> Trait<T1, ..., Tn> | ||
for<lifetimes> Trait(T1, ..., tn) -> Tr | ||
|
||
This syntax can be used in where clauses and types. The `for` syntax | ||
is not permitted in impls nor in qualified paths (`<T as Trait>`). In | ||
impls, the distinction between early and late-bound lifetimes are | ||
inferred. In qualified paths, which are used to select a member from | ||
an impl, no bound lifetimes are permitted. | ||
|
||
## Update syntax of fn types | ||
|
||
The existing bare fn types will be updated to use the same `for` | ||
notation. Therefore, `<'a> fn(&'a int)` becomes `for<'a> fn(&'a int)`. | ||
|
||
## Implicit binders when using parentheses notation and in fn types | ||
|
||
When using the `Trait(T1, ..., Tn)` notation, implicit binders are | ||
introduced for omitted lifetimes. In other words, `FnMut(&int)` is | ||
effectively shorthand for `for<'a> FnMut(&'a int)`, which is itself | ||
shorthand for `for<'a> FnMut<(&'a int,),()>`. No implicit binders are | ||
introduced when not using the parentheses notation (i.e., | ||
`Trait<T1,...,Tn>`). These binders interact with lifetime elision in | ||
the usual way, and hence `FnMut(&Foo) -> &Bar` is shorthand for | ||
`for<'a> FnMut(&'a Foo) -> &'a Bar`. The same is all true (and already | ||
true) for fn types. | ||
|
||
## Distinguishing early vs late bound lifetimes in impls | ||
|
||
We will distinguish early vs late-bound lifetimes on impls in the same | ||
way as we do for fns. Background on this process can be found in these | ||
two blog posts \[[1][1], [2][2]\]. The basic idea is to distinguish | ||
early-bound lifetimes, which must be substituted immediately, from | ||
late-bound lifetimes, which can be made into a higher-ranked trait | ||
reference. | ||
|
||
The rule is that any lifetime parameter `'x` declared on an impl is | ||
considered *early bound* if `'x` appears in any of the following locations: | ||
|
||
- the self type of the impl; | ||
- a where clause associated with the impl (here we assume that all bounds on | ||
impl parameters are desugared into where clauses). | ||
|
||
All other lifetimes are considered *late bound*. | ||
|
||
When we decide what kind of trait-reference is *provided* by an impl, | ||
late bound lifetimes are moved into a `for` clause attached to the | ||
reference. Here are some examples: | ||
|
||
```rust | ||
// Here 'late does not appear in any where clause nor in the self type, | ||
// and hence it is late-bound. Thus this impl is considered to provide: | ||
// | ||
// SomeType : for<'late> FnMut<(&'late Foo,),()> | ||
impl<'late> FnMut(&'late Foo) -> Bar for SomeType { ... } | ||
|
||
// Here 'early appears in the self type and hence it is early bound. | ||
// This impl thus provides: | ||
// | ||
// SomeOtherType<'early> : FnMut<(&'early Foo,),()> | ||
impl<'early> FnMut(&'early Foo) -> Bar for SomeOtherType<'early> { ... } | ||
``` | ||
|
||
This means that if there were a consumer that required a type which | ||
implemented `FnMut(&Foo)`, only `SomeType` could be used, not | ||
`SomeOtherType`: | ||
|
||
```rust | ||
fn foo<T>(t: T) where T : FnMut(&Foo) { ... } | ||
|
||
foo::<SomeType>(...) // ok | ||
foo::<SomeOtherType<'static>>(...) // not ok | ||
``` | ||
|
||
[1]: http://smallcultfollowing.com/babysteps/blog/2013/10/29/intermingled-parameter-lists/ | ||
[2]: http://smallcultfollowing.com/babysteps/blog/2013/11/04/intermingled-parameter-lists/ | ||
|
||
## Instantiating late-bound lifetimes in a trait reference | ||
|
||
Whenever | ||
an associated item from a trait reference is accessed, all late-bound | ||
lifetimes are instantiated. This means basically when a method is | ||
called and so forth. Here are some examples: | ||
|
||
fn foo<'b,T:for<'a> FnMut(&'a &'b Foo)>(t: T) { | ||
t(...); // here, 'a is freshly instantiated | ||
t(...); // here, 'a is freshly instantiated again | ||
} | ||
|
||
Other times when a late-bound lifetime would be instantiated: | ||
|
||
- Accessing an associated constant, once those are implemented. | ||
- Accessing an associated type. | ||
|
||
Another way to state these rules is that bound lifetimes are not | ||
permitted in the traits found in qualified paths -- and things like | ||
method calls and accesses to associated items can all be desugared | ||
into calls via qualified paths. For example, the call `t(...)` above | ||
is equivalent to: | ||
|
||
fn foo<'b,T:for<'a> FnMut(&'a &'b Foo)>(t: T) { | ||
// Here, per the usual rules, the omitted lifetime on the outer | ||
// reference will be instantiated with a fresh variable. | ||
<t as FnMut<(&&'b Foo,),()>::call_mut(&mut t, ...); | ||
<t as FnMut<(&&'b Foo,),()>::call_mut(&mut t, ...); | ||
} | ||
|
||
## Subtyping of trait references | ||
|
||
The subtyping rules for trait references that involve higher-ranked | ||
lifetimes will be defined in an analogous way to the current subtyping | ||
rules for closures. The high-level idea is to replace each | ||
higher-ranked lifetime with a skolemized variable, perform the usual | ||
subtyping checks, and then check whether those skolemized variables | ||
would be being unified with anything else. The interested reader is | ||
referred to | ||
[Simon Peyton-Jones rather thorough but quite readable paper on the topic][spj] | ||
or the documentation in | ||
`src/librustc/middle/typeck/infer/region_inference/doc.rs`. | ||
|
||
The most important point is that the rules provide for subtyping that | ||
goes from "more general" to "less general". For example, if I have a | ||
trait reference like `for<'a> FnMut(&'a int)`, that would be usable | ||
wherever a trait reference with a concrete lifetime, like | ||
`FnMut(&'static int)`, is expected. | ||
|
||
[spj]: http://research.microsoft.com/en-us/um/people/simonpj/papers/higher-rank/ | ||
|
||
# Drawbacks | ||
|
||
This feature is needed. There isn't really any particular drawback beyond | ||
language complexity. | ||
|
||
# Alternatives | ||
|
||
**Drop the keyword.** The `for` keyword is used due to potential | ||
ambiguities surrounding UFCS notation. Under UFCS, it is legal to | ||
write e.g. `<T>::Foo::Bar` in a type context. This is awfully close to | ||
something like `<'a> ::std::FnMut`. Currently, the parser could | ||
probably use the lifetime distinction to know the difference, but | ||
future extensions (see next paragraph) could allow types to be used as | ||
well, and it is still possible we will opt to "drop the tick" in | ||
lifetimes. Moreover, the syntax `<'a> FnMut(&'a uint)` is not exactly | ||
beautiful to begin with. | ||
|
||
**Permit higher-ranked traits with type variables.** This RFC limits | ||
"higher-rankedness" to lifetimes. It is plausible to extend the system | ||
in the future to permit types as well, though only in where clauses | ||
and not in types. For example, one might write: | ||
|
||
fn foo<IDENTITY>(t: IDENTITY) where IDENTITY : for<U> FnMut(U) -> U { ... } | ||
|
||
# Unresolved questions | ||
|
||
None. Implementation is underway though not complete. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current syntax for higher-ranked lifetimes in bare functions is
fn<'a>(&'a int)
, not<'a> fn(&'a int)
. Perhaps bare functions could stay the same,Fn{,Mut,Once}
could use a similar syntax with the sugar (Fn<'a>(&'a int)
), and any other trait would have to use thefor
syntax (for<'a> Foo<&'a int>
).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Fri, Oct 10, 2014 at 02:06:51PM -0700, P1start wrote:
I considered this. The main reason I did not propose it is that it
makes the parsing much more subtle. How do I know whether
Foo<
isgoing to be
Foo<A,B>
orFoo<'a>(...)
? It seems to require quite abit of lookahead, particularly if you don't want to rely on the
lifetime vs type distinction (which I don't, for reasons described in
the RFC). I suppose that there is a cover grammar, nonetheless, so
in principle it doesn't require extraordinary lookahead.
Of course, there is also some appeal to having a single syntax that is
uniformly applicable to all types.
In any case I certainly ought to have included a section on it for the
RFC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we allow the
for <'a>
format at thefn
definition point as well (in addition to the type expression form)?i.e. support as an item
for <'a> fn(x: &'a mut int) -> &'a mut int { *x = *x + 1; x }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pnkfelix That's essentially #122, which was postponed.