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
impl Service for Something {
type Future = impl Future<Output = ...>;
fn service(&mut self) -> Self::Future {
async move {
...
}
}
}
Example: IntoIterator trait
impl IntoIterator for &MyType {
type IntoIter = impl Iterator<Item = Self::Item>;
type Item = ...;
fn into_iter(self) -> Self::IntoIter {
...
}
}
In Embassy, the user writes this:
#[embassy_executor::task]
async fn run() {
// stuff
}
which gets expanded to the following, which references a TAIT Fut
in the type of a constant:
async fn __run1_task() {
// stuff
}
fn run1() -> ::embassy_executor::SpawnToken<impl Sized> {
type Fut = impl ::core::future::Future + 'static;
static POOL: ::embassy_executor::raw::TaskPool<Fut, 1usize> = ::embassy_executor::raw::TaskPool::new();
// defining use here. Signature for `_spawn_async_fn` is:
// impl<F: Future + 'static, const N: usize> TaskPool<F, N> {
// fn _spawn_async_fn<FutFn>(&'static self, future: FutFn) -> SpawnToken<impl Sized>
// where FutFn: FnOnce() -> F;
// }
// so it constrains `Fut` to be the future for `__run1_task()`.
unsafe { POOL._spawn_async_fn(move || __run1_task()) }
}
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).
trait Request {
type Body: Serialize;
// More common case: Associated type is an output of a trait method
fn into_http_request(self) -> http::Request<Self::Body>;
}
trait Response {
type Body: DeserializeOwned;
// Less common case: Associated type is an input of a trait method
fn try_from_http_response(http::Response<Self::Body>) -> anyhow::Result<Self>;
}
/// Request type for `GET /foo/:bar`
#[derive(Request)]
#[request(method = GET, path = "/foo/{bar}")]
pub struct MyHttpRequest {
// path fields
#[request(path)]
bar: String,
// body fields
#[serde(skip_serializing_if = "Option::is_none")]
field_a: Option<i32>,
field_b: SomethingElse,
}
// generated by Request derive
// note: not public
#[derive(Serialize)]
struct MyHttpRequestBody {
#[serde(skip_serializing_if = "Option::is_none")]
field_a: Option<i32>,
field_b: SomethingElse,
}
// also generated by Request derive
impl Request for MyHttpRequest {
type Body = impl Serialize;
fn into_http_request(self) -> http::Request<Self::Body> {
let Self { bar, field_a, field_b } = self;
let uri = format!("/foo/{bar}", bar=bar);
let body = MyHttpRequestBody { field_a, field_b };
http::Request::builder().method(Method::GET).uri(uri).body(body).unwrap()
}
}
/// Response type for `GET /foo/:bar`
#[derive(Response)]
pub struct MyHttpResponse {
#[response(header = http::header::EXPIRES)]
expires: Option<Timestamp>,
#[serde(default)]
field_x: u32,
field_y: SomeData,
}
// generated by Response derive
// note: not public
#[derive(Deserialize)]
struct MyHttpResponseBody {
#[serde(default)]
field_x: u32,
field_y: SomeData,
}
// also generated by Response derive
impl Response for MyHttpResponse {
type Body = impl DeserializeOwned;
fn try_from_http_response(res: http::Response<Self::Body>) -> anyhow::Result<Self> {
let expires = res.headers()
.get(http::header::EXPIRES)
.map(Timestamp::try_from)
.transpose()?;
let MyHttpResponseBody { field_x, field_y } = req.into_body();
Ok(Self { expires, field_a, field_b })
}
}
&'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
macro_rules! singleton {
($val:expr) => {{
type T = impl Sized;
static STATIC_CELL: static_cell::StaticCell<T> = static_cell::StaticCell::new();
// the 1-item tuple is a hack to ensure we return a `&mut T` and
// not a `&mut impl Sized`, which would cause type inference to behave weirdly.
let (x,) = STATIC_CELL.init(($val,));
x
}};
}
let x = singleton!(42u32); // gives a &'static mut u32
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?
impl SomeTrait for MyType {
type SomeType = impl core::fmt::Debug;
fn process(self) {
const X: <MyType as SomeTrait>::SomeType = 22_u32;
}
}
This doesn't work, should it? (Niko thinks no)
#![feature(type_alias_impl_trait)]
use std::ops::Add;
struct MyType;
impl Add for MyType {
type Output = impl std::fmt::Debug;
fn add(self, other: MyType) -> u32 {
22
}
}
fn main() { }
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…
impl<T: Debug> IntoIterator for DebugSampler {
type Item = String;
// type IntoIter not specified
fn into_iter(self) -> Self::IntoIter {
self.inner
.into_iter()
.chunks(self.chunk_size)
.enumerate()
.map(|(i, c)| format!(...))
}
}
The above proposal could be made more explicit with something like
type IntoIter = _;
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:
type IntoIter = _ as impl Iterator<Item = Self::Item>;
If a user wants hiding but not inference, they are free to do that as well:
type Foo = u32 as impl Debug;
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:
fn generate<T>(value: T) -> type 'A {
'A: move || value
}
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?
type Foo = impl Debug;
fn foo() {
// imagine this were allowed:
let x: Foo = 22_u32;
// imagine this were not allowed:
let x: Foo = { let y: Foo = 22_u32; y };
}
pnkfelix: simple example, method with an explicit return type that you inline into a call?
type Internal = impl Debug;
fn produce() -> Internal {
22
}
fn consume() {
let x = produce();
}
// becomes
type Internal = impl Debug;
fn consume() {
let x: Internal = 22; // ERROR
}
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:
type Foo = impl Debug;
impl Iterator for Something {
type Item = Foo;
fn next(&mut self) -> Option<Foo> { }
}
type Foo = impl Debug;
impl Iterator for Something {
type Item = Foo;
fn next(&mut self) -> Option<Self::Item> { }
}
impl Iterator for Something {
type Item = impl Debug;
fn next(&mut self) -> Option<Self::Item> { }
}
not allowed:
type Foo = impl Debug;
impl Iterator for Something {
type Item = Foo;
fn next(&mut self) -> Option<u32> {
}
}
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.
mod foo {
type Foo = impl Debug;
pub fn foo() -> Foo {}
}
let x: ??? = foo();
_ 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?
{
foo::<{ type Bar = impl Sized; let x: Bar = 3; x}>(); // Defining scope?
}
#[defines]
attributetype Foo = impl Debug;
fn foo() defines Foo {
let x: Foo = 42;
}
fn foo() {
let x: def Foo = 42;
}
can 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.
type Foo = impl Debug defined_by bar;
fn bar() {
let x: Foo = 42;
}
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 :(