Stabilize associated type position impl Trait (ATPIT) by traviscross · Pull Request #120700 · rust-lang/rust (original) (raw)
Stabilization report
Summary
We are stabilizing #![feature(impl_trait_in_assoc_type)]
, commonly called either "associated type position impl Trait" (ATPIT) or "impl Trait in associated type" (ITIAT).
Among other things, this feature allows async
blocks and async fn
to be used in more places (instead of writing implementations of Future
by hand), filling gaps in the story of async Rust.
Stabilizing ATPIT helps the many Rust users who have well known use cases, including, e.g., the Tower library (in particular, e.g., its Service trait) and its extensive ecosystem.
The theme of this stabilization report is simplicity. Through much work, we've found that we can stabilize a subset of RFC 2515 that solves the most demanded use cases while enabling a simple and robust implementation in the compiler, supporting efficient implementations in other tooling, and answering all previously-open language design questions in a principled way.
This is a partial stabilization of RFC 2515 and #63063.
What is stabilized
Summary of stabilization
We now allow impl Trait
to appear in type position in associated type definitions within trait implementations.
For the first time, we can now use async
blocks to implement IntoFuture
without boxing. E.g.:
use core::future::{Future, IntoFuture};
struct AlwaysAsync42; impl IntoFuture for AlwaysAsync42 { type Output = impl Iterator<Item = impl Fn() -> u8>; type IntoFuture = impl Future<Output = Self::Output>; fn into_future(self) -> Self::IntoFuture { async { core::iter::repeat(|| 42) }} }
Just as with RPIT, impl Trait
may appear syntactically multiple times within the type, and any item that does not register a hidden type for the opaque witnesses it as an opaque type. No items outside of the impl block may register hidden types for impl Trait
opaques defined within. The rules for which items within the impl block may do so are a simple extension of the RPIT rules and are described below.
Opaqueness
When we say that an item can only use a type opaquely, what we mean is that item can only use the type via the interfaces defined by the traits that the type is declared to implement and those of the leaked auto traits (as with RPIT).
A type that can only be used opaquely by some items is known as an opaque type, or, in context, as simply an opaque. The concrete type or type constructor that underlies the opaque is known as the hidden type, or, in context, as the hidden.
When an item chooses a concrete hidden type to underlie some opaque, we say that it has registered a hidden for that opaque, or equivalently, that it has "defined the hidden type of the opaque", or, more loosely and in context, has "defined the opaque type".
We describe below which items may and must register a hidden type for a given opaque. Other items may only use such types opaquely.
Parallel to RPIT
ATPIT is the extension of RPIT to associated type definitions and trait implementations. Everything that is true about RPIT opaque types is true about ATPIT opaque types modulo that:
- For RPIT, when an
impl Trait
opaque type appears in the signature of an item, that item may and must register a hidden type for that opaque. - For ATPIT, when an
impl Trait
opaque type in the same impl block is syntactically reachable from an associated type in the signature of an item, that item may and must register a hidden type for that opaque.
Syntactic reachability rule
The rule to decide whether an item may and must register a hidden type for an opaque is strictly syntactic and strictly local to the impl block. It's an extension from considering just the signature, as with RPIT, to also considering the definitions of associated types in the same impl.
Intuitively, we collect all impl Trait
types within the same trait impl that are syntactically reachable from the signature of an item, considering only that item's signature and the associated type definitions within the impl block. We don't recurse into ADTs (e.g. the fields of a struct).
More precisely, the rule is as follows:
To determine the set of impl Trait
opaque types for which an item may and must register a hidden type, from the signature (including from any where
clauses), we:
- Collect all types and generic type arguments1 without normalization, and for each, we:
- a. Collect RPIT-like ("existential") uses of
impl Trait
into the set of opaque types for which this item may and must register a hidden type.2 - b. Recurse syntactically (i.e. normalize one step) into the definition of associated types from the same trait when all of the generic arguments (including
Self
) match, and from there, we repeat step 1.
- a. Collect RPIT-like ("existential") uses of
Note that RPIT already performs steps 1 and 1a. The stabilization of ATPIT adds only step 1b.
Example of syntactic reachability rule
Following the rules for syntactic reachability, this works as we would expect:
use core::future::{Future, IntoFuture};
struct AlwaysAsync42; impl IntoFuture for AlwaysAsync42 { type Output = Box<dyn Iterator<Item = impl Fn() -> u8>>; type IntoFuture = impl Future<Output = ::Output>; fn into_future(self) -> Self::IntoFuture { async { Box::new(core::iter::repeat(|| 42u8)) as Box<dyn Iterator<Item = _>> }} }
May define == must define rule
If an impl Trait
opaque type is syntactically reachable from the signature of an item according to the syntactic reachability rule, then a hidden type may and must be registered by that item for the opaque.
Items not satisfying this predicate may not register a hidden type for the opaque.
Sibling only rule
Only the syntactic items that are direct children of the impl block may register hidden types for an impl Trait
opaque type in an associated type. Nested syntactic items within those items may not do so. As with RPIT, closures and async
blocks, which are in some sense items but are not syntactic ones and which share the generics and where
clauses of their parent, may register hidden types.
Coherence rule
Coherence checking is the process by which we ensure that no two trait impls may overlap. To decide that, we must have a rule for whether any two types may be equal, for the purposes of coherence.
During coherence checking, when an opaque type is related with another type we assume that the two types may be equal unless that other type does not fulfill any of the item bounds of the opaque.
Design principles
The design described in this document for ATPIT adheres to the following principles.
Convenience and minimal overhead for the common cases
We believe that impl Trait
syntax in return position and in associated type position is for convenience and should have minimal overhead to use.
The design proposed for stabilization here solves common and important use cases well, conveniently, and with minimal overhead, similar to RPIT, by e.g. leaning on the syntactic reachability rule. Other use cases may prefer to wait for full type alias impl Trait.
Local reasoning
We believe that it should be easy to determine whether an item may and must register a hidden type of an impl Trait
opaque type.
In certain edge cases, whether or not an item may register a hidden type for an opaque can affect method selection and type inference. It should therefore be straightforward for the user to look syntactically at the impl block only to determine whether or not an item may register a hidden type for any opaque. This is what the syntactic reachability rule achieves.
Crisp behavior
We believe that the behavior of impl Trait
should be very crisp; if an item may register the hidden type for an opaque, then within that item the type should act exactly like an inference variable (existential quantification). If it cannot, then within that item the type should act like a generic type (universal quantification).
If items were allowed to register hidden types without being required to do so, then it is believed to be either difficult or impossible to maintain this kind of crispness in all circumstances. Consequently, this design adopts the "may define == must define" rule to preserve this crisp behavior.
Motivation
We long ago stabilized async fn
and async { .. }
blocks as these make writing async Rust more pleasant than having to implement Future
everywhere by hand.
However, there's been a lingering problem with this story. The type of the futures returned by async fn
and async { .. }
blocks cannot be named, and we often need to name types to do useful things in Rust. This means that we can't use async
everywhere that we might want to use it, and it means that if our dependencies do use it, that can create problems for us that we can't fix ourselves.
It's for this reason that using RPIT in public APIs has long been considered an anti-pattern. Using the recently-stabilized RPITIT and AFIT in public APIs carries these same pitfalls while adding a new one: the inability for callers to set bounds on these types.
It is these problems, in the context of interfaces defined as traits, that ATPIT addresses.
Using async
in more places
Today, if we want to implement IntoFuture
for a type so that it can be awaited using .await
, we have no choice but to implement Future::poll
for some wrapper type by hand so that it can be named in the associated type of IntoFuture
.
With ATPIT, for the first time, we can use async { .. }
blocks to implement IntoFuture
. E.g.:
use core::future::{Future, IntoFuture};
struct AlwaysAsync42; impl IntoFuture for AlwaysAsync42 { type Output = impl Iterator<Item = impl Fn() -> u8>; type IntoFuture = impl Future<Output = Self::Output>; fn into_future(self) -> Self::IntoFuture { async { core::iter::repeat(|| 42) }} }
Naming types, expressing bounds, object-safety, and precise capturing
If someone were writing a Service
-like trait today, now that AFIT and RPITIT are stable, that person may think to write it as follows so that async
blocks or async fn
could be used in the impl:
#![allow(async_fn_in_trait)]
pub trait Service { type Response; async fn call(&self) -> Self::Response; }
However, as compared with using associated types, that would create four problems for users of the trait:
- The trait would now not be object-safe.
- The type of the returned
Future
can't be named so as e.g. to store it in a struct. - The type of the returned
Future
can't be named so as to set bounds on it, e.g. to require aSend
, FusedFuture, or TryFuture bound. - The type of the returned
Future
captures a lifetime we may not want to capture.
In the future, there may be other and better solutions to some of these problems (those solutions may in fact desugar essentially to ATPIT). But today, without ATPIT, trait authors face a dilemma. They must either accept all of these drawbacks or, alternatively, must accept that implementors of the trait will not be able to use async
blocks and will have to write manual implementations of Future
.
ATPIT offers us a way out of this dilemma. Trait authors can allow implementors to use async
blocks (and conceivably, in the future, async fn
) to implement the trait while preserving the object safety of the trait, allow users to name the type so as to store it and set bounds, and express precise capturing of type and lifetime generic parameters.
Related issues
- Fix "higher kinded" -> "higher ranked" in RFC 3498 rfcs#3590
- higher ranked lifetimes not recognized by type alias impl trait #96146
- nested RPIT and HRTB: unclear semantics and future incompatibility #96194
- support higher-ranked regions in opaque type inference #100503
- Tracking Issue for impl for<'a> Trait<'a> #104288
- defining type-alias-impl-trait inside closures is broken #105498
- ICE when equating TAIT and RPIT in canonical queries #108498
- RPIT allows defining use with invalid args #111935
- check for non-defining uses of RPIT #112842
- "correctly" check opaque type lifetimes to be unique params #113916
- opaque type definition: strict lifetime equality vs equal-by-inference #113971
- Incorrect lifetime bound check in async + impl_trait_in_assoc_type #114572
- RPIT hidden types can be ill-formed #114728
- stricter hidden type wf-check #115008
- go through uses of DefineOpaqueTypes::No and either document or change them #116652
- Prevent opaque types being instantiated twice with different regions within the same function #116935
- rework opaque type region inference #116891
- Tracking Issue for Lifetime Capture Rules 2024 (RFC 3498) #117587
- Decision on "must define before use" for opaque types #117866
- [crater] Normalize opaques to infer vars eagerly in AssocTypeNormalizer #120798
- test that we do not support higher-ranked regions in opaque type inference #121386
- some smaller DefiningOpaqueTypes::No -> Yes switches #121394
- DefineOpaqueTypes::No in UFC Self resolution #121404
- stricter hidden type wf-check [based on #115008] #121679
- Make DefiningAnchor::Bind only store the opaque types that may be constrained, instead of the current infcx root item. #121796
- Pass list of defineable opaque types into canonical queries #122077
- nested TAITs/ATPITs should reject higher-ranked regions #122093
- Better comment for implicit captures in RPITIT #122100
- Make TAITs and ATPITs capture late-bound lifetimes in scope #122103
- Do not try to reveal hidden types when trying to prove auto-traits in the defining scope #122192
- Correctly register lifetime constraints from opaque types from canonical queries called from nll #123035
- [WIP] Enforce may-define-must-define for ATPITs #123046
- Avoid a scrape_region_constraints and instead register the region constraints directly #123669
- More DefineOpaqueTypes::Yes #123794
- change method resolution to constrain hidden types instead of rejecting method candidates #123962
- Some unstable changes to where opaque types get defined #124080
- Allow coercing functions whose signature differs in opaque types in their defining scope into a shared function pointer type #124297
- Allow constraining opaque types during various unsizing casts #125610
- Do not define opaque types when selecting impls #126258
- Do not eagerly reject inference vars when trying to resolve method calls. #126316
- APITIT get treated as ordinary params in "not all trait impls implemented" error #126395
- WIP: eliminate DefineOpaqueTypes by using Yes across the compiler #127034
- revealing uses in the defining scope: merge borrowck for all typeck children types-team#129
Open items
"Must define before use"
- Opaque types in Rust today, including stable RPIT, allow themselves to be used opaquely in a body before a hidden type is registered for the opaque. There is a proposal to reject this (tracked in #117866). This is orthogonal to ATPIT except to the degree that ATPIT would provide new ways for people to write this sort of code. We're exploring this restriction in parallel with this proposed stabilization. Regardless of the FCP completing on this issue, we'll ensure that restriction is first merged or otherwise disposed of before we merge this PR.
Add Projection
/NormalizesTo
goals for opaque types
- Add
Projection
/NormalizesTo
goals for opaque types to fix the issue raised here. Even though the example linked there results in an ICE, the ICE only happens when trying to exploit the unsoundness; the overlapping impls are simply accepted incorrectly. There is no known generally-applicable way to exploit this into actual unsoundness, but we treat accepting overlapping impls as unsound by itself. We'll ensure this is handled before merging, regardless of completion of the FCP.
Update the Rust Reference
- Regardless of the FCP completing on this issue, we'll ensure that there is an accepted PR for updating the Rust reference before merging this PR.
Acknowledgments
The stabilization of ATPIT would not have been possible without, in particular, the ongoing and outstanding work of @oli-obk, who has been quietly pushing forward on all aspects of type alias impl Trait for years. Thanks are also due, in no small part, to @compiler-errors for pushing forward both on this work directly and on critical foundations which have made this work possible. Similarly, we can't say enough positive things about the work that @lcnr has been and is doing on the new trait solver; that work has shaped this proposal and is also what gives us confidence about the ability to support and extend this feature into the long term.
Separately, the author of this stabilization report thanks @oli-obk, @compiler-errors, @tmandry, and @nikomatsakis for their personal support on this work.
Thanks are due to the types team generally for helping develop the "Mini-TAIT" proposal that preceded this work. And thanks are of course due to the authors of RFC 1522, RFC 1951, RFC 2071, and RFC 2515, @Kimundi, @aturon, @cramertj, and @varkor, and to all those who contributed usefully to those designs and discussions.
Thanks to @nikomatsakis for setting out the design principles articulated in this document, and to @tmandry, @oli-obk, and @compiler-errors for reviewing drafts. All errors and omissions remain those of the author alone.
Footnotes
- ADTs with generic parameters, non-unit tuples, arrays, slices, pointers, references, function pointers, and
impl Trait
anddyn Trait
types with generic parameters or associated types are type constructors. Traits with generic parameters or associated types, including theFn*
traits, are bounds constructors. When we say, "generic type arguments" above, we mean types that are used to parameterize these type constructors or bounds constructors, including for associated types. When we collect generic type arguments, we do so syntactically (i.e. without normalization and looking only at what is written) and recursively (i.e. within arbitrary levels of syntactic composition). ↩ - There are existing restrictions on where an RPIT
impl Trait
may appear within a type and within a signature. Those restrictions remain and are not being changed by this stabilization. ↩