or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Syncing
xxxxxxxxxx
Note for later readers:
The consensus reached through this discussion resulted in RFC 3498. The RFC encompases and supersedes this document. Except for reasons of historical interest, it would be better to read the RFC.
Introduction
Rust's rules around capturing lifetimes in opaque and hidden types are inconsistent, unergonomic, and not helpful to users. In common scenarios, doing the correct thing requires a "trick" that most people don't know about. Of the people who do know about this trick, most of them do not fully understand it.
As we look forward to the 2024 edition and move toward stabilizing features such as type alias
impl Trait
(TAIT), associated type positionimpl Trait
(ATPIT), return positionimpl Trait
in trait (RPITIT), andasync fn
in trait (AFIT), we must decide on a clear vision of how lifetimes should be captured in Rust.We want the upcoming features in the stabilization pipeline to capture lifetimes in a way that's consistent with each other and with the way we want Rust to work and develop going forward.
This meeting is about finding and building consensus on that shared vision of the lifetime capture rules. This meeting will be a success if we can agree in principle to how we want lifetimes to be captured in Rust going forward and whether it's more important for new features to be aligned with that direction than to be aligned with the behavior in Rust 2021. Such an agreement in principle will unblock important work on these features and will lay the groundwork for an RFC to be written formalizing these capture rules.
Background
Capturing lifetimes
In return position
impl Trait
andasync fn
, an opaque type is a type that can only be used for its specified trait bounds (and for the "leaked" auto trait bounds of its hidden type). A hidden type is the actual concrete type of the values hidden behind the opaque type.A hidden type is only allowed to name lifetime parameters when those lifetime parameters have been "captured" by the corresponding opaque type. For example:
In the above, we would say that the lifetime parameter
'a
has been captured in the return type.When a caller receives an opaque type and wishes to prove that it outlives some lifetime, the caller must prove, unless the opaque has a
+ 'other
bound stating the opaque outlives that lifetime (after transitively taking into consideration other known lifetime bounds), that all of the captured lifetime components of the opaque type outlive that lifetime. The captured lifetime components are the set of lifetimes contained within captured type parameters and the lifetimes represented by captured lifetime parameters.Instead of saying the above, people have previously said that the opaque type outlives the intersection of the captured lifetimes.
Conversely, a bound like
impl Trait + 'a + 'b
was said to require that the opaque type outlive the union of those captured lifetimes.While this framing has some appeal in terms of type theory and subtyping relationships, there's some debate about its usefulness in describing Rust semantics. We mention it here only because earlier discussions and references may have used this terminology.
Capturing lifetimes in type parameters
In return position
impl Trait
andasync fn
, lifetimes inside of type parameters may also be captured. For example:In the above, we would say that
foo
captures the type parameterT
or that it "captures all lifetimes contained in the type parameterT
". Consequently, the call tofoo
captures the lifetime'a
in the return type.Behavior of
async fn
As we saw in the examples above,
async
functions automatically capture all type and lifetime parameters in scope.This is different than the rule for return position
impl Trait
(RPIT) which requires that lifetime parameters (but not type parameters) be captured by writing them in the bound. As we'll see below, RPIT requires users to use aCaptures
"trick" to get the correct behavior.The inconsistency is visible to users when desugaring from
async fn
to RPIT. As that's something users commonly do, users have to be aware of this complexity in Rust today.For example, given this
async fn
:To correctly desugar this to RPIT, we must write:
(As we'll discuss below, other seemingly simpler desugarings are incorrect.)
If async had happened first, we could imagine that the lifetime capture rules for RPIT may have gone the other way.
Lifetimes in scope from an outer impl are also captured automatically by an
async fn
. For example:Note that the lifetime is captured whether or not the lifetime appears in the return type and whether or not the lifetime is actually used in the hidden type at all.
Working with the lifetime capture rules in RPIT
For the borrow checker to function with an opaque type it must know what lifetimes it captures (and consequently what lifetimes may be used by the hidden type), so it's important that this information can be deduced from the signature, either by writing it out or by an automatic rule.
As we saw in the previous example, for RPIT (but not
async fn
), the rule is that opaque types automatically capture lifetimes within the type parameters but only capture lifetime parameters when those lifetime parameters are mentioned in their bounds.When someone wants to capture a lifetime parameter not already in the bounds, that person must use one of the "tricks" we'll describe next.
The outlives trick
Consider this example:
This does not compile today because the
'a
lifetime is not mentioned in the bounds of the opaque type. We can "fix" this by writing:This called the "outlives trick". But this "solution" is a lie. To see this, think about what
impl Sized + 'a
means: we return an opaque type that implementsSized
and outlives'a
.But outliving
'a
was never actually a promise we wanted to make to our API user. In fact, it was quite the opposite. We wanted to say that our opaque type captures'a
. In other words,'a
outlives our opaque type (if lifetimes could be said to outlive types in Rust).Counterintuitively, adding an outlives relationship in the wrong direction satisfied our needs. This works for two reasons:
'a
. The direction doesn't matter if the lifetimes are equal.Nevertheless, it is also problematic for two reasons:
The
Captures
trickThere is another, more "honest" way to accomplish this. It's the only option when there are multiple lifetimes in play (including via automatically captured type parameters), especially when those lifetimes may be invariant. It's called the
Captures
trick. Consider again our example:We could instead solve the problem like this:
Every type trivially implements
Captures<T>
for any typeT
, so we're not adding any new meaningful trait bounds. But we are affecting the lifetime bounds of the opaque type. And because we have now named the lifetime in the bounds, the lifetime parameter can be captured by the hidden type.We can extend this trick to multiple lifetimes (and to other positions[1]). For example:
The biggest problem with
Captures
is that it is clunky both to write and to read, and it comes with a high overhead of reasoning about "why" it exists and is necessary.Inconsistency: Capturing type parameters vs lifetime parameters in RPIT
As mentioned above, capturing applies to type parameters too. Unlike lifetime parameters, in RPIT, opaque types capture all type parameters in scope.
This is relevant when the type parameters themselves contain lifetimes.
For example, if we had a
chain
helper defined like this:We could call it and use it like so:
Moreover, it would be an error to drop
a
orb
before the for loop, becausechained
captures borrows of each via its type parametersT
andU
.Overcapturing
What if the rule that causes us to capture all type parameters causes us to capture too much? The solution is type alias
impl Trait
(currently available only on nightly).As we'll see below, this is how we can also solve the overcapturing of lifetime parameters that happens on stable Rust today with
async fn
and that could occur under RPIT with a revised semantic.For an example of overcapturing with RPIT in stable Rust today, see Appendix D.
Summary of problems
In summary, we have identified a number of problems with the state of things today:
async fn
and this is exposed to users because of the common need to switch between the two forms.The solution
We propose to make the rules for RPIT match the rules for
async fn
exactly in the 2024 edition.For the upcoming features to be stabilized in the 2021 edition, we propose to align them with the proposed 2024 edition RPIT/
async fn
captures behavior.Apply
async fn
rule to RPIT in 2024 editionUnder this proposal, in the 2024 edition of Rust, RPIT would automatically capture all lifetime parameters in scope, just as
async fn
does today, and just as RPIT does today when capturing type parameters.Consequently, the following examples would become legal:
Capturing a lifetime from a free function signature
Capturing a lifetime from outer inherent impl
Capturing a lifetime from a method signature
TAIT as the solution to overcapturing
As we saw above, sometimes the capture rules result in unwanted lifetimes being captured. This happens today in Rust due to the existing RPIT rules for capturing lifetimes from all in-scope type parameters and the
async fn
rules for capturing all in-scope type and lifetime parameters. Under the proposal, lifetime parameters could also be overcaptured by RPIT.The solution to this is type alias
impl Trait
(TAIT). It works as follows. Consider this overcaptures scenario under the proposed semantic.In the above code, we want to rely on the fact that
foo
does not actually use any lifetimes in its return type. We can't do that using RPIT because it captures too much. However, we can use TAIT to solve this problem elegantly as follows:Type alias
impl Trait
(TAIT)Under this proposal, the opaque type in type alias
impl Trait
(TAIT) will automatically capture all lifetimes present in the type alias. For example:Associated type position
impl Trait
(ATPIT)Under this proposal, the opaque type in associated type position
impl Trait
(ATPIT) will automatically capture all lifetimes present in the GAT and in the outer impl.For example:
Return position
impl Trait
in Trait (RPITIT)Under this proposal, the opaque type in return position
impl Trait
in trait (RPITIT) will automatically capture, in the trait definition, all trait input lifetimes, all lifetimes in theSelf
type, and all lifetimes in the associated function or method signature.In trait impls, RPIT will automatically capture all lifetime parameters from the outer impl and from the associated function or method signature. This ensures that signatures are copyable from trait definitions to impls.
For example:
async fn
in trait (AFIT)Under this proposal, the opaque type in
async fn
in trait (AFIT) will automatically capture, in the trait definition, all trait input lifetimes, all lifetimes in theSelf
type, and all lifetimes in the associated function or method signature.In the trait impls, AFIT will automatically capture all lifetime parameters from the outer impl and from the associated function or method signature. This ensures that signatures are copyable from trait definitions to impls.
Signatures are directly copyable from the trait definitions to the impls.
This behavior of AFIT will be parsimonious with the current stable capture behavior of
async fn
.For example:
Conclusion
The lifetime capture rules in Rust today are inconsistent, unergonomic, and unhelpful to users. Users commonly get this wrong and can't figure out what to do.
We can make a decision today to make these rules more consistent and helpful in the 2024 edition for RPIT.
If we make that decision, then we can unblock current work on upcoming feature stabilizations and allow those upcoming features to align with the future direction of the language's capture rules.
Appendix A: Other resources
Other resources:
Earlier obsolete drafts:
Appendix B: Matrix of capturing effects
async fn
In the table above, "LP" refers to "lifetime parameters".
The 2024 behavior described for all items is the behavior under this proposal.
The 2021 behavior described for RPIT and
async fn
is the current stable behavior. The other 2021 behaviors described are the proposed behaviors that would be implemented for the features ahead of stabilization.All of the feature above automatically capture all lifetimes from all type parameters in scope in both the 2021 and the 2024 editions.
Appendix C: The need for an inconsistency between inherent RPIT and RPITIT
Under today's semantics, inherent RPITs do not capture any lifetimes automatically.
If we were to apply this rule directly to RPITIT, we'd quickly end up in an unworkable situation.
Where would we put
+ 'a
(or+ Captures<'a>
) in the above code to make it compile? Notice that the trait has no way of naming'a
at all, because that is part of theSelf
type it knows nothing about.Given the current rules, our options are:
The current rules lead to some kind of inconsistency in any case. But the proposed rules would obviate the problem and allow RPIT to be fully consistent, whether it is used in an inherent impl or a trait.
Appendix D: Overcapturing of type parameters in stable RPIT
As an example of how overcapturing may occur in RPIT in Rust today due to the automatic capturing of type parameters, consider consider this overcaptures scenario:
On stable Rust today there's no way to fix that. However, with TAIT, we can write:
Appendix E: The outlives trick fails with only one lifetime parameter
This appendix has been added after the meeting in response to discussion during the meeting.
People often think that the outlives trick is OK as long as there is only one lifetime parameter. This is not in fact true. Consider:
Appendix F: Adding a
'static
boundThis appendix has been added after the meeting in response to discussion during the meeting.
Adding a
+ 'static
bound will work in Rust 2024 in exactly the same way that it works in Rust 2021.Appendix G: Future possibility: precise capturing syntax
This appendix has been added after the meeting in response to discussion during the meeting.
This proposal intends for people to use TAIT to avoid capturing type and lifetime parameters that should not be captured. If this comes up too often in the future, we may want to consider adding new syntax to
impl Trait
to allow for precise capturing. One proposal for that would look like this:This is not part of this proposal.
Questions / Discussion
(Have a question? Add a new section here.)
data around existing usage?
nikomatsakis: AFAIK we don't have a TON of data from crates.io, unless I missed something in the doc, but I did do one experiment around this. I searched through
-> impl Trait
in the compiler. I found that literally every usage wanted to capture lifetimes with 2 exceptions, both instances of the same pattern, theindices
function. Is there more data available?TC: I spent some hours searching GitHub for instances of
-> impl .* \+ ''
and reading through the findings looking for examples of where it might have been used "correctly" rather than because the author didn't know about the captures trick. I didn't keep count, but I looked through a lot of code but didn't find any examples of where the author shouldn't have just usedCaptures
(and I edited and rebuilt each to confirm).joshtriplett: given that
indices
specifically returns+ 'static
, that suggests that if we could handle+ 'static
people could potentially avoid TAIT in that simple case.compiler-errors: What's the issue with
indices
? Since it explicitly outlives'static
, changing its captures shouldn't matter. We can always use a'static
bound even if we overcapture something.nikomatsakis: Question for the room: What % of code do you think needs to prefer this to give you pause about changing the default?
joshtriplett: Something like 20%, except that having to do a TAIT is pretty bad, so I wouldn't want more than 1% of code having to do that.
tmandry: I agree TAIT is a bit clunky, but it makes it very easy to reason about the capture rules using the syntax of your code.
pnkfelix: To clarify
TC: I looked at the top crates on Github and in every case they wanted to use
Captures
instead of+ 'a
.pnkfelix: It sounds like no one needs
+ 'a
and if there were an ergonomic way to expressCaptures
people should use that instead?nikomatsakis: I can think of places, e.g. in Rayon where you need
+ 'a
pnkfelix: Seems unfortunate that the slickest syntax has been reserved for something that seems niche, at best. Can't do anything about it.
timeline for stabilization
nikomatsakis: I am in favor of this proposal. I do have a concern. Specifically, I really want to move TAITs and RPITITs towards stabilization. Suppose we align on the goals of this doc and therefore decide to change TAIT/RPITIT capture rules to "capture everything in scope". Do we feel we need an accepted RFC to modify
-> impl TRait
before we can stabilize those, because the premise is that it will align with the bigger question? What level of assurance do we want. And how quickly can we move towards an accepted RFC, presuming we don't get instantaneous concerns raised.joshtriplett: Yes, but this doc would be 99% of that RFC; put a bit of RFC substrate around it, file it, FCP it.
TC: If we can get an agreement in principle here, I've offered to write that RFC (I'd guess that tmandry would want to collaborate there), and I agree this document is already most of that.
TC: Also, answering this big but simple question makes a lot of the complex and hairy questions of those feature go away. That can have the net effect of speeding things up.
tmandry: I'm not too worried about RFC'ing but don't want a 1% standard
joshtriplett: To clarify, what I meant was that if it's >1% it would give me pause and I would want a more ergonomic solution. Not that we should block this decision on that.
nikomatsakis: my take would be: that motivates us putting effort, but doesn't block stabilizing what we have.
joshtriplett: we should clarify this in the RFC.
edition tenets
nikomatsakis: Just for the record, in RFC 3085, which introduced the Rust 2021 edition, we established some "tenets" around editions. One of them was:
as well as
I believe that these two together imply that we should favor "uniformity across editions" over "uniformity within older editions".
pnkfelix: I agree we should favor consistency across editions
tmandry: it also requires us to answer a lot more subquestions if we try to have uniform behavior within editions rather than across
joshtriplett: I'd be wary of a blanket rule, but I think it applies here because we think it's already the behavior people want.
clarification
nikomatsakis: The text states:
Although this is indeed the current behavior of the compiler, it is not exactly true. For example, consider this function:
We can conceivably prove that this opaque type outlives some lifetime
'x
even if'a
doesn't outlive'x
. There's some subtlety here because of the way we currently define outlives in a syntactic way, this exact example (with the explicitCaptures
) clause introduces some complications related to RFC 1214 (not sure if that's the right RFC #). This is kind of a side note I think and doesn't impact the main thrust of the point.compiler-errors: For the record, I clarified this
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →TC: compiler-errors: Could you suggest the correct wording there? I rewrote that section according to your clarification (but of course, all errors are my own).
CE: i did above, it's rough but it's just enumerating the choices one has in RFC 1214.
TC: What would fill in for ??? below?
CE: idk, something like "either all the captured components of the opaque outlive that lifetime, or the opaque has an explicit item bound stating that it outlives that lifetime". trying to state it in a single sentence rather than an enumerable list is the problem here, lol.
TC: Going to edit this in:
tmandry: I feel like stating the precise rules is beside the main point of the doc, personally.
CE:
The precise rules are partly why people can get away with(edit: probably not relevant)+'a
in the bounds in some cases.CE: idk, I agree with Niko above that this is an important clarification. Also, the outlives bound thing works currently.
history of async fn vs impl trait
nikomatsakis: Another historical side note. The text notes:
Back when we were designing
async fn
, there was also a question of whether we should doasync fn foo() -> u32
orasync fn foo() -> impl Future<Output = u32>
. I think we probably picked right because the latter is very verbose, but many systems (e.g., C#) have opted to go the other way, and the jury is still out. It's also perhaps inconsistent with what atry fn
might look like, which doesn't have an obvious "carrier" in the same way. At the time, the argument that I found most persuasive was precisely the fact thatasync fn
capture rules were different from-> impl Future
, which seemed to seal the deal to me.random edition thoughts
nikomatsakis: If we were to introduce this change over an edition
reserving question spot
pnkfelix: (not sure if doc addresses this eventually but I want to ensure I write it down)
pnkfelix:
what is the way to opt out from the new implicit captures? e..g if I want the currentoh, answer is TAIT.semantics, for e.g.
fn foo<'a, T>(x: &'a (), y: T) -> impl Trait { y }
, where I don't want'a
captured by the RPIT, how do I express that?compiler-errors: Yeah, TAIT is probably the suggestion here.
pnkfelix:
now wondering about TAIT vs ATPIT.never mind, the'f
is explicitly wired to the'g
in the example there.Explicit syntax for "captures"?
Would an explicit syntax for captures be useful here? For instance,
-> impl<'a, 'b> Trait
? Premise: if you explicitly name captures you don't get any captures you don't name, so if you want the 2021 behavior you can write-> impl<> Trait
rather than using a TAIT. (May not be perfect, since there may be lifetimes you can't name.)Handling
+ 'a
joshtriplett: I think it's very likely that people will continue to write
+ 'a
. Should we do something to recognize where doing so may be redundant/unnecessary/harmful?TC: It'd be interesting to explore heuristics we could use to lint about this.
joshtriplett: Also, can we present a reasonably concise example that demonstrates how
+ 'a
(and separately+ 'a + 'b
) does the wrong thing? This document attempts to explain the difference in terms of the opaque type outliving the lifetimes versus being outlived by the lifetimes, and that+ 'a
effectively causes both which makes the return value equal to the lifetimes, but would it be possible to get an example showing code that doesn't manage to get workable code by using+
?TC: Here's an example from my notes:
compiler-errors: An example demonstrating
+ 'a + 'b
(where'a
and'b
are unrelated) would be to show that any hidden type would have to outlive'static
to be valid. Not exactly sure how to show it other than "here's a bunch of code that doesn't work", though.joshtriplett: What happens if you have a thing that outlives
'a
and'b
but isn't static? Obvious example:compiler-errors: Because you need to prove that
&'b str: 'a
, and&'a str: 'b
in order for the hidden type to be valid. (and&'a str: 'a
,&'b str: 'b
, but those are trivial). A hidden type must uphold all of the bounds in the opaque, incl all outlives bounds.joshtriplett: To ask it another way: what would happen if
impl Display + 'a + 'b
just meant "outlives'a
and outlives'b
" but didn't mean "is outlived by"?compiler-errors: By "and", do you mean the lower-bound of those two lifetimes, or the upper-bound? I don't exactly know what you're asking
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →(upper bound; "outlives
'a
and outlives'b
)compiler-errors: Or do you mean, what would happen if
+ 'a
means "captures" only? I don't really like the wording "lifetime outlives type", it's confusing and not precise imo.joshtriplett: let's take this offline to Zulip, perhaps, and focus on the current discussion.
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →Question: Are we ready to RFC this?
TC: If we were to propose an RFC with the text of this document, cleaned up lightly for the RFC format, and with the addition that Josh proposed (that if in the future we discover that too much code experiences overcapturing and needs to be desugared to TAIT, we should explore more ergonomic solutions), who here would check their box, by a show of hands?
joshtriplett: Hand.
tmandry: Hand.
nikomatsakis: Hand.
pnkfelix: Hand.
(scottmcm was not present in the meeting.)
EOF
The
Captures
trick is useful in positions other than return positionimpl Trait
. For example, if you ever wanted to say that a lifetime outlived a generic type parameter, you probably could have used theCaptures
trick to allow the type parameter to name the lifetime. This is because we allow generics to name lifetimes that appear in their bounds. Example ↩︎