fn foo(x: impl Trait)
)fn foo() -> impl Trait
)trait Foo { fn bar() -> impl Baz
)type Foo = impl Bar
at module levelimpl Foo for Bar { type Baz = impl Qux; }
impl Trait
desugars to an opaque type that has a hidden type which must meet the declared bounds; note that a TAIT (or RPIT, etc) may have multiple opaque types (e.g., type Foo = (impl Bar, impl Baz)
).let
(e.g., let x: TAIT = 22_u32
), a return (e.g., return 22_u32
in a function whose return type is TAIT
), or in other ways.The defining characteristic here is that you have a simple trait that returns a future, iterator, or something like it.
Example: Tower service trait
Example: IntoIterator trait
In Embassy, the user writes this:
which gets expanded to the following, which references a TAIT Fut
in the type of a constant:
Based on the real-world request and response traits from Ruma, which the project is planning to switch from converting from one-step HTTP deserialization to two steps:
http::Request<SomeRawByteType>
(or Response
) to http::Request<RequestTypeBodyFields>
This is done for two reasons:
See this PR (which was merged, but not into the main branch; latest rebase here).
&'static mut
macroConverts from T
to &'static mut T
without alloc. (Sort of equivalent to Box::leak
, except you can run it only once). source
A defining use for an opaque type O appearing in a TAIT or an AssocIT is…
Rationale: You can define a TAIT in exactly those places where you could write impl Trait
and have it desugar to a TAIT.
Advantages: Easy to explain what a defining use is.
Disadvantages: Supports all examples except for the "jplatte" example. This example doesn't work because the opaque type only appears as a method.
Commitments: Not impossible to add #[defines]
later, but harder; the story is muddled. When do you need #[defines]
exactly and why?
Question: do we accept this?
This proposal keeps TAITs unstable and only stabilizes AssocIT only. A defining use would be any impl that is a constraining use. This accepts all the impl examples. It does not accept embassy, though embassy can be rewritten to use this on stable (it is sort of awkward).
Rationale: Modules are different categorically from other items (including most impls). For most items, they are small, and when you use impl Trait
there, it's natural that the "scope" that can constrain it is any place within that item. In contrast, modules often contain distinct "groupings" of items that belong together but for which it is not convenient to declare a distinct module. Let's worry about that case later, and focus just on the impl case to start.
Advantages: All examples work, albeit with some annoyance to manage true TAITs.
Disadvantages: TAITs remain unstable; we lose the analogy of an impl body to a module.
Commitments: We cannot add #[defines]
later on impls, though we could lint for it and recommend its use if experience found that it is common to have confusion due to impls like the jplatte example.
#[defines]
on both TAIT and AssocITA defining use for an opaque type O appearing in a TAIT or an AssocIT is…
#[defines(X)]
(for a TAIT) or #[defines(Self::X)]
(for an AssocIT)Advantages: Explicit
Disadvantages: Verbose, especially for cases like the impl; not clear that this adds value.
Also, if we use #[defines]
for AssocIT, we have to add ability to deal with types in attributes, decide if we want to normalize, etc. If we just limit #[defines]
to TAITs, we only think about paths. Regardless we don't have a ton of precedent here.
Commitments: We can add more natural forms later, of course, but then we have more options.
This works today, should it?
This doesn't work, should it? (Niko thinks no)
Here are some other points on the design space that might be useful as we consider issues with the current proposal.
Some use cases can be addressed by simply not requiring the user to write the associated type value…
The above proposal could be made more explicit with something like
Notably, inference would not include hiding details of the inner type. This might be desired in cases where a user actually wants to reveal those details, but can't or doesn't want to write the type. Hiding could still be accomplished with impl Trait
, and these syntax proposals can even be combined:
If a user wants hiding but not inference, they are free to do that as well:
Then we would still have the question of what is considered a defining use, but it could be scoped to explicit uses of _
and not implied by any use of impl Trait
. That way if there are nonobvious type inference behaviors that result from use of _
you only "pay for what you use".
Inspired by this blog series: https://david.kolo.ski/blog/a-new-impl-trait-2/.
The above series suggests going further and removing type inference-based mechanisms altogether, replacing them with some explicit way of labelling the definition site of a type with a placeholder name:
This is like taking #[defines]
past the function and going all the way down to the expression level.
Any constraining item within the defining scope that is not a defining use is a hard error. This means we can later opt to allow such a use; or to allow it with an annotation of some kind; or to make other such changes.
To make sure I follow this, is it true that any such use could be made into a defining use? Or are we saying that in some places within the defining scope, you can get "stuck" into a compile error you can't really do anything productive about?
pnkfelix: simple example, method with an explicit return type that you inline into a call?
simulacrum: yeah, I could see this being annoying and confusing. Feels easy to get in a situation where you are stuck.
pnkfelix: could imagine that you have to add the #[defines]
attribute to accept that case.
simulacrum: I feel like I don't have a good enough grasp on the limitations to say if that helps or not, but I can feel that most users will not read the rules and understand them, so they'll self-discover them based on usage.
pnkfelix: compiler could give you a nice error message here, right? Where would #[defines]
go, anyway?
oscherer: should be on the function to get true benefit
nikomatsakis: part of the confusing is that the defining use / constraining use terminolgoy is there, it's meant to be a temporary thing to "hold space" for future decisions.
summary of the concern:
Whatever the rational, limitations, etc are: I would like to be able to refactor my code in ways. Particularly since as you refactor you'll get complex errors. You might get the error quite late after you've fixed a bunch of other things.
oscherer: error would occur early.
pnkfelix: would we be able to provide good feedback for defines attribute?
oscherer: moment you have a mismatch with an opaque type, we could suggest adding the attribute
allowed:
not allowed:
We can end up having a function nameable, but not its return type, even though
Adts must be public if used in a public API.
_ as impl Trait
proposalnikomatsakis: I don't hate the _ as impl Trait
family of proposals, actually, but I'm also loath to revisit the design in such a big way. Is there a forwards compatible path to that if we ever want to do something like that?
(scottmcm: Going even further and having things like Add
default to inferring their Output
is tempting too – re-specifying it for those single-method traits is mostly just noise.)
nikomatsakis: The #[defines]
attribute makes the "defining uses" explicit, but people still can't specify the "hidden type", which I guess is where the as impl Trait
syntax comes into play. Are there other forms of implicitness not being specified here?
#[defines]
attributecan also use the attribute and desugar to unstable syntax
k#defines
?scottmcm: Rather than adding syntax to everything, we could have it special on type
. That'd make the grammar change more scoped.
That has the nice behaviour that if you're a human wondering what the type actually is, you can also look at the place that the type alias tells you to look. (In addition to being easier for R-A too.)
(Oh, or even as part of the impl Trait
syntax, interesting.)
niko: How do we specify that it's constrained by an impl?
scottmcm: Maybe UFCS? That does mean it's not just a path, though :(