TAIT: type alias impl trait
type-alias-impl-trait allows moving the already stable impl Trait
s (that are only legal in function return types) into type aliases and thus be able to use them in more places and multiple times.
fn foo() -> impl Trait {
value_of_type_that_implements_Trait
}
// The above function can be changed to the
// following without a change in behaviour for callers.
type Foo = impl Trait;
fn foo() -> Foo {
value_of_type_that_implements_Trait
}
In contrast to return-position-impl-trait, this type alias can be used as the return type of multiple functions, but all functions doing so must use the same hidden type. This is similar to how all code paths in a function with a return-position-impl-trait must return the same type:
fn foo() -> impl Debug {
if true {
return 42;
}
"42" // ERROR: another return site returns an `i32`
}
type Bar = impl Debug;
fn bar() -> Bar {
42
}
fn boo() -> Bar {
"42" // ERROR: another function uses `i32` for `Bar`
}
In contrast to return-position-impl-trait, these type aliases can also be used in other locations.
Without knowing the hidden type, we can still use the opaque type and use its trait bounds. In this case we can use the Debug
trait to render it:
fn bop(x: Bar) {
println!("{x:?}");
}
Binding a hidden type works in both directions, not just assigning a hidden type value to the opaque type, but also reading an opaque type into a hidden type value:
fn bup(x: Bar) {
let x: i32 = x;
}
This does not "reveal" the hidden type. It binds an explicitly known i32
type as the hidden type of Bar
and will error if that's not the hidden type everywhere else, too.
As a last usage, you can avoid binding any hidden types and just use the type-alias-impl-trait by just forwarding it elsewhere:
fn burp(x: Bar) -> Bar {
x
}
You can also use type-alias-impl-trait for the type
of local variables, constants, statics, …
let x: Bar = 42;
const X: Bar = 42;
static Y: Bar = 42;
You can use type-alias-impl-trait in other types:
struct MyStruct {
bar: Bar,
}
and use it just like other uses of Bar
:
fn foo(my_struct: &MyStruct) {
println!("{:?}", my_struct.bar);
}
fn new() -> MyStruct {
MyStruct {
bar: 42
}
}
Since type-alias-impl-trait can be referenced anywhere a type alias could be, this also means you can use them in impl
blocks:
type Foo = impl Trait;
impl Bar for Foo {}
There's a huge caveat though: now it's possible for there to be an impl for an opaque type and its hidden type:
type Foo = impl Trait;
fn foo() -> Foo {}
impl Bar for Foo {}
impl Bar for () {} // ERROR conflicts with `impl Bar for Foo`
This check is not done by revealing the hidden type, but by checking whether a type could be a hidden type for that specific opaque type. So the following program is legal:
trait Trait {}
impl Trait for () {}
type Foo = impl Trait;
fn foo() -> Foo {}
impl Bar for Foo {}
impl Bar for i32 {}
This is legal, because i32
could not possibly be a hidden type of Foo
, because it doesn't implement Trait
wich is a requirement for all hypothetical hidden types of Foo
.
This is tested very thoroughly and is actually the simplest sound implementation for opaque types in coherence. While we could be more restrictive (just outright forbidding opaque types), that's not actually simpler from a compiler perspective and it's a neat kind of feature to support, even if we don't know the use case yet.
Associated types can also be type-alias-impl-trait (associated-type-impl-trait?):
impl Deref for MyType {
type Target = impl Trait;
fn deref(&self) -> &Self::Target {
&self.field
}
}
While this example is fairly artificial, the real benefit is when you have unnameable types like async
blocks:
impl IntoFuture for MyType {
type Output = ();
type Future = impl Future<Output = ()>;
fn into_future(self) -> Self::Future {
async move {
// do stuff here
}
}
}
This way you do not need to write burdensome Future
impls yourself. Similarly with complex Iterator
implementations.
Similar to return-position-impl-trait, you can only bind a hidden type of a type-alias-impl-trait within a specific "scope" (henceforth called "defining scope"). The defining scope of a return-position-impl-trait is the function's body, excluding other items nested within that function's body (we may want to relax that restriction on return-position-impl-trait in the future).
The defining scope of a type-alias-impl-trait is the scope in which it was defined. So usually a module and all its child items, but it can also be a function body, const initializer and similar scopes that can define items.
Any use of the type-alias-impl-trait within the defining scope will become a defining use (meaning it binds a hidden type), if the type is coerced to or from, equated with, or subtyped with any other concrete type. Usages that rely solely on the trait bounds of the type are not considered defining. Similarly, usages that just pass a value of a type-alias-impl-trait around into other places of the type-alias-impl-trait type are not considered defining.