Tracking issue for RFC 2515, "Permit impl Trait in type aliases"
Centril opened this issue · comments
This is a tracking issue for the RFC "Permit impl Trait in type aliases" (rust-lang/rfcs#2515) which is implemented under the following #![feature(..)]
gates:
type_alias_impl_trait
impl_trait_in_assoc_type
: #110237
About tracking issues
Tracking issues are used to record the overall progress of implementation.
They are also uses as hubs connecting to other relevant issues, e.g., bugs or open design questions.
A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature.
Instead, open a dedicated issue for the specific matter and add the relevant feature gate label.
Steps
- Implement the RFC
- Adjust documentation (see instructions on rustc-guide)
- Stabilization PR (see instructions on rustc-guide)
Unresolved questions
-
Exactly what should count as "defining uses" for opaque types?
- Should the set of "defining uses" for an opaque type in an impl be just items of the impl, or include nested items within the impl functions etc? (see here for example)
- should return-position-impl-trait also start allowing nested functions and closures to affect the hidden type? -- independent question to be answered separately by the lang team. Would simplify compiler.
-
can we come up with consistent rules when cross-usage type inference can happen?
-
compiles on stable, even though there is no obvious type for
fn foo(x: bool) -> impl Debug { if x { return vec!["hi"] } Default::default() }
Default::default()
to produce a value of. We combine all return sites though and compute a shared type across them, so we'll figure out aVec<&'static str>
impl Foo
can be used for associated types that expect a type that implementsBar
, even ifFoo
andBar
are entirely unrelated. The hidden type must satisfy both. See https://github.com/rust-lang/rust/pull/99860/files for examples.
-
-
impl traits in consts through const fns are allowed but shouldn't be: #87277
@varkor See my comment about the PR for this in the other thread, and let me know if you'd like me to take up your existing PR and finish it off maybe. :-)
@alexreg: thanks for the offer — I'll try to get a PR open this week, but if I turn out not to have enough time, I may pass it on to you :)
It appears that the current implementation does not trigger any "private type in public interface" errors, for the following code (playground):
#![feature(existential_type)]
#[derive(Debug)]
struct Bar(usize);
existential type Foo: std::fmt::Debug;
pub fn foo() -> Foo {
Bar(5)
}
I would expect it to give an error like
error[E0446]: private type alias `Foo` in public interface
--> src/lib.rs:8:1
|
5 | existential type Foo: std::fmt::Debug
| - `Foo` declared as private
...
8 | / pub fn foo() -> Foo {
9 | | Bar(5)
10 | | }
| |_^ can't leak private type alias
(although, because normal type aliases are transparent to the visibility checking, I'm not certain exactly what the error message should say; it just seems bad to introduce another way to accidentally forget to make part of your API publicly nameable)
EDIT: Opened #63169
Got a tiny question: Is type Foo = impl Debug
equivalent to type Foo = impl Debug + '_
or type Foo = impl Debug + 'static
?
In #63092, landed on 2019-07-28, written by @Centril and reviewed by @varkor, the tracking issue for existential_type
was adjusted to this one.
In #63096, landed on 2019-07-29, written by @Centril and reviewed by @varkor, 3 ICEs were closed with reproducer tests added.
In #63158, landed on 2019-07-31, written by @JohnTitor and reviewed by @Centril, 1 ICE was closed with a reproducer test added.
In #63180, landed on 2019-08-03, written by @varkor and reviewed by @Centril, the syntax was changed to type Foo = impl Bar;
through a temporary hack in the parser. @varkor will follow up to remove that hack. The new feature gate name is type_alias_impl_trait
and the old one existential_type
has been removed.
In rust-lang/rustc-dev-guide#402, landed on 2019-08-29, written by @varkor and reviewed by @mark-i-m, @Arnavion, and @spastorino, the rustc guide description was updated.
Given that async/await is now available on stable, not being able to name futures returned from async functions seems like one of the bigger factors limiting where async/await can be used (without boxing). What are the remaining blockers to stabilizing this?
From the comments, it seems it has been implemented and at least some documentation has been written (though this is not reflected in the initial description's task list yet).
@djc Please see the associated label F-type_alias_impl_trait
. As you can see, there are a lot of bugs and the implementation is partial. Beyond fixing all of those bugs, the set of tests will need to be audited, and moreover there are unresolved questions as the issue description notes. This is far from in a ready state to stabilize.
There's currently an unfortunate trade-off with usages of type-alias-impl-trait (TAIT from here on out). You can either:
- Use a TAIT within the defining scope. This requires your function to be fully generic over the TAIT usage (cannot repeat type parameters, have extra bounds, on parametesr to the TAIT, etc).
- Use a TAIT outside the defining scope. This allows you to use any type or type parameter for the generic parameters of the TAIT, but you cannot (by design) see the underlying type.
This means that moving code into a defining scope can actually make it stop compiling, as it might not be fully generic over the TAIT.
This is a significant limitation - it prevents a large amount of perfectly reasonable code from being written, and contradicts the general principle that moving code into a 'more private' scope (e.g. into a crate, module, or function) is always valid.
I see two ways of working around this:
- Allow these kinds of not-fully-generic uses of TAITs. Under the current rules of the RFC, this means that function body can no longer see the underlying type (since every usage must either fully constrain the TAIT, or leave the TAIT fully unconstrained). This would allow more code, but would still prohibt reasonable things like:
type MyType<T, V> = impl Copy;
fn make_it<T>(val: T) -> MyType<T, T> { Ok(val) /* ERROR: we cannot see the underlying type here */ }
- Amend the RFC to allow some kind of 'partial constraining' of the TAIT. This would need to work within the following constraints:
a) The underlying type may be unnameable (e.g. a closure). Thus, we cannot assume that it's always possible to specify the underlying type when the opaque type is defined (which would allow us to remove the concept of 'defining use').
b) Type-checking should not become insanely complicated. In particular, we probably want to avoid anything that would require function bodies to be type-checked in a specific order (e.g. to find the one containing the 'fully defining' use, to assist with 'partially defining' uses).
type MyType<T, V> = impl Copy; fn make_it<T>(val: T) -> MyType<T, T> { Ok(val) /* ERROR: we cannot see the underlying type here */ }
Function that don't qualify to see the underlying type can use private helper function, which won't be horrible uneconomic as long as turbofish isn't required (but turbofish is currently required, not sure if that's a dup of #66426 or not).
Also I think option 2 can be backward-compatibly added even after stabilization.
I've found an interesting use for impl trait in type alias: treating specific functions as regular types. I'm not even sure how legal it's supposed to be, but it's partially working, partially rejected by compiler and partially causes ICEs: https://play.rust-lang.org/?version=nightly&gist=f4e134cea703a8a8fbaf6aef687a56f2
I don't follow @Aaron1011's argument. The following is illegal, because it does not fully constrain MyType
:
type MyType<T, E> = impl Copy;
fn make_it<T: Copy>(val: T) -> MyType<T, T> { Result::<T, T>::Ok(val) }
The following is also illegal, in this case because a defining use of MyType
must exist within the same scope:
mod M {
pub type MyType<T, E> = impl Copy;
}
fn make_it<T: Copy>(val: T) -> M::MyType<T, T> { Result::<T, T>::Ok(val) }
Note however that this is legal:
type MyType<T> = impl Copy;
fn make_it<T: Copy>(val: T) -> MyType<T> { Result::<T, T>::Ok(val) }
Uses outside the defining scope cannot assume any more than the trait bounds (e.g. with the above, Copy
is available on values of MyType
but nothing else).
I believe that your last example should actaully be illegal: #52843 (comment)
No, Aaron, because here MyType<T>
is an alias for Result<T, T>
which has no bounds on its generics.
Oh, you're right. I missed the fact that MyType
didn't resolve to T
, like in the example in #52843 (comment).
However, my point about restrictions within the defining scope still stands. Usaages within the defining scope cannot repeat generic parameters, instantiate generic parameters with concrete types, or place bounds on the underlying type. I think we should try to come up with a solution that would allow this kind of code in defining scopes.
You are partially right it seems: partial defining uses within the scope are not allowed, although in theory in combination with a full defining usage (or possibly even multiple partial defining usages) it could be legal. Perhaps this should be an extension?
I have yet to find code which works in the outer scope but not the inner scope. Some examples:
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=987d8f7823abbef1505de2fe0ad86ab3
@dhardy: Here's an example of code which works in the outer scope, but not the inner scope:
Move produce_it
into the inner scope to break it.
Concern: the defining implementation cannot currently be a struct field. (In part this overlaps with @Aaron1011's concern of being unable to use the type within the defining scope except in defining uses.)
#![feature(type_alias_impl_trait)]
type A = impl Drop;
struct S {
a: A,
}
fn foo() -> S {
S { a: Box::new(1) }
}
Concern: it is not possible to define type aliases without trait bounds using the current syntax (because type A;
is not an existential type). Although unbounded existentials (without even Drop
) are useless, it would be nice for them to be legal (in the same way that a spurious let
binding or unused struct field is legal). Unfortunately I see no solution to this using the RFC-proposed syntax.
Concern: scopes. I believe it was agreed that the details of an existential type should not cross API boundaries (i.e. escape from modules/crates). On the other hand, I believe it would be useful for the type details to be able to escape the defining scope (within the same module, the same as non-pub
types are accessible within the same module). This would allow struct literal macros to be significantly more useful.
Concern: the defining implementation cannot currently be a struct field.
Please refile this as an issue so that we can have targeted in-depth discussion about this aspect of the type inference / checking.
Concern: it is not possible to define type aliases without trait bounds using the current syntax
#![feature(type_alias_impl_trait)]
type A = impl Sized;
fn f1() -> A { 0 }
type B = impl ?Sized;
fn f2() -> &'static B { &[0] }
type C = impl ?Sized + 'static;
fn f3() -> &'static C { &[0] }
(This would be a good test for someone to add.)
On the other hand, I believe it would be useful for the type details to be able to escape the defining scope
I believe you want type Foo = _;
(rust-lang/rfcs#2524).
Thanks @Centril for reminding me of 2524. I'm still unsure whether type details should be available outside the defining scope (the RFC is not really clear).
?Sized
is an interesting type bound, but your test cases should really include the following (currently passes):
type B = impl ?Sized;
fn foo() -> &'static A { &1 }
Done: #66980
I haven't found this bug reported somewhere thus I don't know if it's known.
Nevermind, found the issue #58011 after searching for "existential type"
, I'm sorry for the noise...
It's not possible to use rustdoc
as it's dropping the function bodies with. Using the latest example in this issue:
trait Bar {}
impl Bar for u8 {}
type Foo = impl Bar;
fn foo() -> Foo {
10
}
results in this error:
$ cargo doc
error[E0277]: the trait bound `(): Bar` is not satisfied
--> src/lib.rs:6:1
|
6 | type Foo = impl Bar;
| ^^^^^^^^^^^^^^^^^^^^ the trait `Bar` is not implemented for `()`
|
= note: the return type of a function must have a statically known size
@TimDiekmann: thanks, this is any one of these (seemingly duplicate) issues.
Another example (from #68368) of trying to write a non-defining use from within a defining scope (note that this code currently ICEs due to an unrelated issue):
#![feature(type_alias_impl_trait)]
pub type Parser<'a, X, T, V> = impl Fn(X) -> Result<(T, V), String>;
pub fn take_cpredicate<'a>(
predicate: impl Fn(char) -> bool,
) -> Parser<'a, &'a str, &'a str, char,> {
move |s| {
if s.len() == 0 {
return Err(s.to_string());
}
let mut chars = s.chars();
let next = chars.next().unwrap();
if predicate(next) {
Ok((&s[1..], next))
} else {
Err(s.to_string())
}
}
}
take_cpredicate
will not compile, since it's a non-defining use (all of the Parser
generic parameters are substituted) in a defining scope.
Since a closure is being used as the underyling type, it's impossible to have more than one fully defining use (since each closure has a unique type). This means that there's really no way of making this code compile, even though it seems like it would be fine if it did.
I just came across an implementation oddity - this compiles (playground):
#![feature(type_alias_impl_trait)]
#![feature(const_generics)]
type Foo<const X: usize, const Y: usize> = impl Sized;
fn foo<const N: usize>(x: [u8; N]) -> Foo<N, N> {
x
}
However, the type parameter and lifetime parameter cases emit very different errors.
The type parameter error is from type_of
:
rust/src/librustc_typeck/collect/type_of.rs
Lines 410 to 420 in 5574b1d
While the lifetime parameter error is from wfcheck
:
rust/src/librustc_typeck/check/wfcheck.rs
Lines 925 to 936 in 5574b1d
I think wfcheck
runs first, and checks for concrete types/consts, but ignores parameters.
But then, for the concrete type case, why does the error come from type_of
and not wfcheck
?!
At least concrete consts do error in wfcheck
.
I've left some comments on the PR that added the checking to type_of
(#57896 (comment)) and I'll now attempt to untangle this, and I suppose add some const
parameter examples to the tests.
EDIT: opened #70272, which should address all of these inconsistencies.
A question about impl Trait in trait type aliases: has any thought been given about how they should handle defaults?
I'm trying to do something like this, without success:
#![feature(type_alias_impl_trait, associated_type_defaults)]
use std::fmt::Debug;
struct Foo {
}
trait Bar {
type T: Debug = impl Debug;
fn bar() -> Self::T {
()
}
}
impl Bar for Foo {
type T = impl Debug;
fn bar() -> Self::T {
()
}
}
I'd guess this just hasn't been RFC'd yes, but does someone know if thought on the topic have already started?
Also, just going to link as a related issue #66551 ; as it almost immediately happens when returning Future
from trait functions, which I assume is an important intended usage of this feature: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=ebb2911238ff57bf988bc322474013d7
This is the first I've discovered this RFC, which is a little unfortunate but such is life. I do have some comments/observations, but I am inclined not to actually argue the RFC is wrong.
I'm going to go off of Centril's excellent comment here as reference to the current state of thinking on this.
In the case of type Foo = impl Bar;, it does not seem like there's noteworthy power that the simple syntax gives up. In fact, as shown before, the proposed syntax seems to compose better than other suggestions.
Associated type defaults are a case where this syntax does truly give something up. You cannot write this without true existentials (I've left out the bounds and function for clarity):
trait IntoIterator {
type Item;
type Iter: Iterator = impl Iterator<Item = <Self as IntoIterator>::Item>;
}
This is not permitted in the current implementation (note that the error message is bad and this should be a TODO), nor would it be particularly useful. This would require the trait author to constrain the type to a single concrete type, which all trait implementors would have to use. But on the other hand, if this were an existential, it would essentially say "trait implementors can provide a type, but it will be opaque if they do not". This is not merely a sugary nicety: it would allow the trait definer to add the type without breaking backwards-compatibility, which they may be unable to do otherwise.
I'm not sure that means that the design in the RFC is wrong, as there are good arguments for the current design being the more accessible one. But I wanted to raise the example anyway, even if it just creates future work to add an alternative syntax for true existentials, like exists<T>
to complement for<'a>
, is available for those rare cases where it's genuinely useful. (Plus I think an awkward syntax discourages use in favour of the nicer syntax proposed here, and that's possibly a good thing.)
@alercah the problem here would be that there's two possible meanings for impl trait in associated type position.
Digression on that topic
Case 0: Associated Type Impl Trait (just TAIT for an associated type), which currently works
trait Factory {
type Item;
fn produce(&mut self) -> Self::Item;
}
impl Factory for Foo {
type Item = impl Debug;
fn produce(&mut self) -> Self::Item {
()
}
}
Case 1: Default Associated Type Impl Trait (also requires feature(associated type defaults)
), currently forbidden
type Factory {
type Item = impl Debug;
fn produce(&mut self) -> Self::Item { () }
}
The two interpretations:
-
@Ekleog's: the TAIT is uniquely constrained by the default implementation(s) of functions on the trait. Implementors of the trait see it as an opaque type (unless per normal TAIT rules they see inside it (which I think they wouldn't because the scope is the trait declaration)), and thus cannot unify with it. For this to be useful, all methods returning the type must be defaulted and overriden together with the associated type.
-
@alercah's: the TAIT default is equivalent to writing copy/pasting the associated type definition into each trait impl. As such, each trait impl gets a unique existential, and the interaction of defaulted methods returning the defaulted associated type is unclear. The intent is likely that default method implementations are "just" copy/pasted into the trait impl and participate in existential inference there. This is not enough by itself, as then trait definitions cannot be type checked until instantiation time. (Generics are not C++ templates; Rust does not have post-monomorphization errors.) The principled decision would be to just not allow default methods to return a DATIT, as the type is not known at that point (but this makes any argument for this under backwards compatibility of the type basically moot; the instantiation has to bound the type). The middle ground would be to... basically make it behave as in @Ekleog's version of DATIT. If the existential is concretized by default method impls, it behaves as a concrete opaque existential to impls, and they cannot unify types with it. At that point, the only difference between this and @Ekleog's version is the ability to have DATIT without a default method constraining it. E.g. being able to lift from
trait Factory { fn make() -> impl Debug; }
to
trait Factory { type Item = impl Debug; fn make() -> Self::Item; }
but not to add a default method returning that associated type.
I suspect this version of DATIT is what would be needed to support async fn
(essentially just RPIT) in traits, so it is likely we will eventually see it. (Explicit DATIT with defaulted method bodies should probably be opaque to the implementors, and need to be overriden as a group.)
But given that this issue is about TAIT and not DATIT, and DATIT hasn't been RFCd (as far as I've seen), further discussion of the DATIT extension to TAIT (with associated type defaults) should probably move to irlo and/or the RFCs repo; this isn't really the place to
I've created a new issue regarding surprising behavior with async fns: #76039
(EDIT: previously the comment was here, moved to own issue since tracking issues are not for discussion)
I came across a case that may be a bug and/or may be linked to #76039.
This code doesn't compile with a "mismatched types" error:
type Callback = impl Fn();
fn callback() {
return;
}
fn register(_cb: Callback) {
return;
}
fn main() {
let cb = || callback();
register(cb);
}
When defining the type directly in the function prototype, it does compile (playground).
@haroldm The error is correct. The feature is for callee-defined types, whereas you're trying to use it as a type parameter, ie a caller-defined type.
Any update on this topic?
Why wouldn't this compile? playground
pub trait Trait<'a, 'b> {
type Assoc1: Debug + 'a;
type Assoc2: Debug + 'b;
fn func1(&'a self) -> Self::Assoc1;
fn func2(&'b self) -> Self::Assoc2;
}
#[derive(Debug)]
struct A<'a> {
a: &'a str,
}
#[derive(Debug)]
struct B<'b> {
b: &'b str,
}
struct Test(String);
impl<'a, 'b> Trait<'a, 'b> for Test {
type Assoc1 = impl Debug + 'a;
type Assoc2 = impl Debug + 'b;
fn func1(&'a self) -> Self::Assoc1 {
A { a: &self.0 }
}
fn func2(&'b self) -> Self::Assoc2 {
B { b: &self.0 }
}
}
The compile error is also confusing:
error[E0477]: the type `impl Debug` does not fulfill the required lifetime
--> src/main.rs:26:5
|
26 | type Assoc1 = impl Debug + 'a;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: type must outlive the lifetime `'a` as defined on the impl at 25:6
--> src/main.rs:25:6
|
25 | impl<'a, 'b> Trait<'a, 'b> for Test {
| ^^
error[E0477]: the type `impl Debug` does not fulfill the required lifetime
--> src/main.rs:27:5
|
27 | type Assoc2 = impl Debug + 'b;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: type must outlive the lifetime `'b` as defined on the impl at 25:10
--> src/main.rs:25:10
|
25 | impl<'a, 'b> Trait<'a, 'b> for Test {
| ^^
error: aborting due to 2 previous errors
Can we stabilise this in its current form, and then add more defining uses later?
Judging by the "mentioned this issue" traffic this seems like an important missing feature.
@ijackson Based on other open issues labeled with F-type_alias_impl_trait
it looks like the implementation may be currently too buggy to be stabilized, regardless of how many people want it.
@ijackson Based on other open issues labeled with
F-type_alias_impl_trait
it looks like the implementation may be currently too buggy to be stabilized, regardless of how many people want it.
Urgh. Thanks for pointing out what I should have looked for myself. Hmmmm.
I have created a min_type_alias_impl_trait
feature gate (WIP PR: #82898) that only permits type alias impl trait in function and method return positions. Modulo a few bugs that I'm also fixing right now, this should be easier to stabilize than the full feature.
@oli-obk Awesome! Thank you for working on that.
@oli-obk is that expected, that all code that was using #![feature(type_alias_impl_trait)]
now broken, because it also requires #![feature(min_type_alias_impl_trait)]
now?
#![feature(type_alias_impl_trait)]
pub type Empty<T> = impl Iterator<Item = T>;
pub fn empty<T>() -> Empty<T> {
None.into_iter()
}
error[E0658]: `impl Trait` in type aliases is unstable
--> src/lib.rs:3:21
|
3 | pub type Empty<T> = impl Iterator<Item = T>;
| ^^^^^^^^^^^^^^^^^^^^^^^
|
= note: see issue #63063 <https://github.com/rust-lang/rust/issues/63063> for more information
= help: add `#![feature(min_type_alias_impl_trait)]` to the crate attributes to enable
warning: the feature `type_alias_impl_trait` is incomplete and may not be safe to use and/or cause compiler crashes
--> src/lib.rs:1:12
|
1 | #![feature(type_alias_impl_trait)]
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(incomplete_features)]` on by default
= note: see issue #63063 <https://github.com/rust-lang/rust/issues/63063> for more information
I would expect #![feature(type_alias_impl_trait)]
to include #![feature(min_type_alias_impl_trait)]
.
I would expect
#![feature(type_alias_impl_trait)]
to include#![feature(min_type_alias_impl_trait)]
.
While that could be done, it is a bit annoying to get such a scheme right. As it's an unstable feature, I didn't think about it more. If it's really a pain point we could implement this, but historically the diagnostics about getting the feature gate wrong in that case have been quite suboptimal (with const_fn vs min_const_fn we had the situation where you did not get a const_fn suggestion if you had min_const_fn active).
I've been experimenting a bit with using min_type_alias_impl_trait
to achieve (sort of) ad-hoc newtyping in Rust.
Example:
#![feature(min_type_alias_impl_trait)]
trait AddSelf: std::ops::Add<Output = Self> + Sized {}
impl<T> AddSelf for T where T: std::ops::Add<Output = Self> {}
type MyI64 =
impl std::fmt::Debug
+ std::fmt::Display
+ AddSelf
+ Copy
+ From<i64>
+ Into<i64>;
fn my_i64(value: i64) -> MyI64 { value }
allowing things like
// compiles
let a = my_i64(23);
let b = my_i64(42);
println!("{} + {} = {}", a, b, a+b);
let x = MyI64::from(2i64 * a.into());
println!("{}", x);
// fail to compile
let x = a + 3i64;
let x = a * a;
Now, it's possible that I got a bit caught up in my excitement and there are serious downsides to this that I'm missing, but if that use-case is intended/supported that would be great. It might have been discussed more in-depth somewhere else and I've missed it.
Why I'm writing this: I'm wondering if it were possible for type errors regarding MyI64
to mention the type alias in the messages, or in a separate note, or simply have the source span declaring the impl Trait
include the alias part?
On a positive note, after experimenting with min_type_alias_impl_trait
for a bit this minor diagnostics inconvenience is the only thing I ran into.
I understand this might be complicated and out of scope for the min_*
stabilization, of course.
It would be really sweet, if i can do this.
pub trait MyTrait<T>: Sized {
type Type = impl Future<Output = T> + Send;
}
Then i don't need to box my futures.
@petar-dambovaliev The trait definition needs to specify a bound. The syntax you used goes in the impls.
@petar-dambovaliev The trait definition needs to specify a bound. The syntax you used goes in the impls.
I want to set a default for the associated type.
One random worry I thought of:
There's currently two ways to effectively seal a trait:
trait A {
// requires naming an unnameable type
fn sealed(_: Voldemort);
}
// requires implementing an unnameable trait
trait B: Voldemort {
}
The latter still works in the face of type = impl Trait;
, but the former no longer prevents implementation of the trait once you can use TAIT to name the parameter type.
This seems like the correct behavior for TAIT, but we should make sure to communicate that the former method for pseudo-sealing the trait won't work once full TAIT is available.
@CAD97 do you have an example of how your trait A
will be affected by TAIT? The type alias is treated as a different type to the concrete type it aliases, so even with extra public API to allow creating a TAIT of the Voldemort
type it's not possible to implement the trait: playground.
Ah, it seems there are two things in the way of this causing a problem.
- I assumed that use as a trait method argument would be enough to be a defining use (as it does limit it to be one type), and
- I assumed that a TAIT constrained in this fashion would be allowed to be used as the function argument.
It's a question of to whom exactly the TAIT is opaque. It's obviously not opaque to the defining usage(s). So long as trait method argument use isn't considered a defining use (and you can make an argument that it could be), and the TAIT's use in this position is always in the opaque region rather than the defining/transparent region, then TAIT won't break this method of pseudo-sealing traits.
Is there a list of known bugs, blocking min_type_alias_impl_trait
from being stabilized? This feature means that literally hundreds of Box::pin
s can be removed from the async ecosystem, and writing low-level async code becomes waay more ergonomic as this also eliminates the need for many custom future types. Of course, without GATs they can't be completely eliminated, but this still represents a huge step forward towards real async fns in traits and I know that many of us are eagerly .await
ing it. The only unresolved question listed here is regarding defining uses, but I don't think that applies to the minimal feature gate .... @oli-obk is there a release that can be a reasonable target for min_type_alias_impl_trait
landing on stable?
@CAD97 note that with full TAIT you can name the type (it still doesn't allow you to implement the trait though):
type V = impl Any;
fn setv<T: A>() {
fn teq<T>(_: &T, _: &T) {}
let x = todo!();
let y = todo!();
teq(&x, &y);
T::sealed(x);
let _f = move || -> V { y };
}
A bit unorganized: https://hackmd.io/tB3V8MP5S66zHIWTUdRvQw but it holds all current info I think. I also created a github label for Tait, but idk how complete the list of tagged things is. I won't work on any of this until July though
Thanks for the link, and thanks for all of your amazing work on this! I look forward to seeing it stabilize soon.
What's preventing progress on this? Can I help out in pushing it to completion?
I am actively working on it. Since I'm in the middle of a big refactoring, I don't think there are any smaller things to work on right now. I'll post an update when I get the refactoring merged (hopefully in two weeks).
Any status updates on this?
Right now, not being able to properly name function types is still a huge bummer, and another big case I ran into is the lack of any meaningful way to name the type of iter.map(Into::into)
, to avoid having to write my own boilerplate. I was considering offering such an adapter to libstd to get around this (for example, iter.cloned()
has a dedicated type even though it's equivalent to iter.map(Clone::clone)
), but if we're close enough that this would likely stabilise first, I'd say it's probably not worth it.
Although maybe such adapters would be worth it anyway for convenience; not really sure.
The PR ran into some unexpected corner cases and had to be reverted, i'll post a new one as soon as the known problems have been addressed and will run crater then. Even if merged, it'll bake a while on nightly and need 6-12 weeks after stabilization on nightly in order to hit stable
For the semi-detached reader of this issue, it might not be clear which PR you are referring to (since that PR doesn't seem to have reference this tracking issue?). I think you might mean #92007?
For the semi-detached reader of this issue, it might not be clear which PR you are referring to (since that PR doesn't seem to have reference this tracking issue?). I think you might mean #92007?
Take 2 of that was merged a couple of days ago: #94081. What's left to be done? @oli-obk, if you comment an update or (preferably) update the issue so anyone unfamiliar with the progress can learn about what has happened
I am currently in the process of resolving the unsoundness bugs left for type alias impl trait. After that I'm going to go through the list of open issues related to type alias impl trait (
F-type_alias_impl_trait
Is this feature a blocker for async trait methods? If so, and with GATs (seemingly) nearing stabilization, this would be the last big piece to unblock that work.
I'm not sure if this is the right place to comment, or if it's too late to change the syntax. But here is a suggestion:
Using this as an example:
trait Foo {}
type Bar = impl Foo;
fn baz(arg: Bar) -> Bar {
// ...
}
One issue of this syntax is that by looking at bar
itself, we can't know if Bar is a concrete type and if the argument type is the guaranteed to be the same type as the return.
I propose we change the syntax to:
trait Foo {}
type impl Bar = impl Foo;
fn baz(arg: impl Bar) -> impl Bar {
// ...
}
This way, we always know if Bar is a type or a set of types satisfying some constraint.
To me, TAIT is more like a trait, therefore deserves a similar syntax as trait, instead of concrete type. Using a more complicated example:
type Foo = Arc<impl Iterator<Item = impl Debug>>;
is more like
trait Foo {}
impl<I1,I2> Foo for Arc<I1>
where I1: Iterator<Item = I2>
I2: Debug {}
In your first example, Bar
is already guaranteed to be the same type in argument and return position. It is a type alias for a single opaque type, for basically the same reason that a function returning impl Foo
can only return one concrete type. One syntactic impl Trait
gives you one opaque type; making an alias for it and using it in multiple places does not change that.
Maybe I'm misunderstanding the rules. Will this compile under current rfc?
type Foo = impl Debug;
fn bar(a: Foo) -> Foo {
3i32
}
fn baz() {
let x = bar("str");
}
And what about this slightly different example?
type Foo = impl Debug;
fn bar(a: Foo) {
println!("{:?}", a);
}
fn baz() {
bar("str");
bar(3i32);
}
If I understand rust correctly, both of these will compile if we use impl Debug
in place of Foo
.
And if I understand the rfc correctly, they will also compile if we use Foo
.
@lijiaqigreat An impl trait alias is a single, opaque, concrete type. What you are thinking of is a different feature that doesn't exist.
Now that GATs have been stabilized, I hope that stabilizing this is on the horizon. I hope to use them together.
What you are thinking of is a different feature that doesn't exist.
Can you explain what the difference is? I also have @lijiaqigreat's assumed understanding of what's being proposed.
For example, does this:
type Foo = impl Debug;
fn bar(a: Foo) -> Foo {
3i32
}
desugar to this?:
fn bar<T: Debug>(a: T) -> impl Debug {
3i32
}
or desugar to this?:
fn bar<T: Debug>(a: T) -> T {
3i32
}
or something else?
@metasim Neither, as this isn't just syntax sugar. You can think of it as working like this:
type Foo = impl Debug;
fn bar() -> Foo {
3i32
}
This is equivalent to this:
type Foo = i32;
fn bar() -> i32 {
3i32
}
with the exception that anyone using the name Foo
is only allowed to use features of the Debug
trait, and is not allowed to assume that Foo
and i32
are actually the same. Only the interior of bar
is allowed to do anything that assumes that Foo
is actually a i32
.
Note that when you write type Foo = impl Debug;
, Foo
only ever is equivalent to exactly 1 type. It is not generic and it cannot represent multiple types. It must statically resolve to exactly 1 type during compilation. For your more complicated example:
type Foo = impl Debug;
fn bar(a: Foo) -> Foo {
3i32
}
This effectively resolves to this:
type Foo = i32;
fn bar(a: i32) -> i32 {
3i32
}
except that because argument a
is listed as type Foo
, callers of bar
are not allowed to supply just any i32
as an argument, because again, the compiler does not allow them to know that Foo
is really just i32
. This of course would make this particular bar
function impossible to use, as you would need to get a Foo
from somewhere, and as written, the only place the caller can get a Foo
is from the return value of bar
.
I am nominating this feature for stabilization.
Stabilization Report
Since this is a big feature, and it changed significantly over the last year, I wrote up a doc explaining the current status of the feature: https://hackmd.io/tB3V8MP5S66zHIWTUdRvQw
The feature's tests are mostly in https://github.com/rust-lang/rust/tree/master/src/test/ui/lazy-type-alias-impl-trait and https://github.com/rust-lang/rust/tree/master/src/test/ui/type-alias-impl-trait, though some (if mostly related to another issue and just using type-alias-impl-trait) are sprinkled across the test suite.
The feature is tracked in https://github.com/orgs/rust-lang/projects/22/views/1 and while there are still 17 open issues, these are mostly about edge cases that we could allow in the future, but I'd like to track them under separate feature gates and stabilize them at a later point.
Some of those issues marked as "after stabilization" are plain ICEs, not future-possibility limitations. Shouldn't those bugs be fixed first, before this feature is stabilized? #104551, #103666, and #99945: they ICE on the latest nightly, rely on no nightly features other than type_alias_impl_trait
, and are likely to occur in real-world code.
I am working on them and they are a priority, but none of them are common uses, and the last two will just become errors.
I don't think they should block starting the stabilization progress, which, considering a lot of ppl will be on vacation soon, will likely take until February anyway.
In the "Binding types" section of the explanation it mentions impl Trait
in const
and static
items and let
bindings, which is tracked in #63065. Is it also intended to be stabilized at this time?
No, only impl Trait
in type aliases and associated types is stabilized. You can use those type aliases in the types of consts and statics though
I think the check for trait impl
s on the type alias needs to be more restrictive, otherwise adding a trait implementation will become a breaking change:
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=df54044199bc21c63d2565bad97072cb
I think the check for trait
impl
s on the type alias needs to be more restrictive, otherwise adding a traitimpl
is a breaking change:
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=df54044199bc21c63d2565bad97072cb
is the error a bug when uncommented the impl SomeTrait for i32 {}
?
I think the check for trait
impl
s on the type alias needs to be more restrictive, otherwise adding a traitimpl
is a breaking change:
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=df54044199bc21c63d2565bad97072cbis the error a bug when uncommented the
impl SomeTrait for i32 {}
?
Yes, although it's not really a bug, just an unwanted feature 😉.
Libaries are already expected to treat adding trait impls as semver-breaking. TAIT is just introducing another way for it to be breaking.
(It's not mentioned in https://doc.rust-lang.org/cargo/reference/semver.html , but there's an open issue about adding it.)
@Arnavion Adding a trait impl is usually only "minor" breakage, which means you can fix it by just adding type annotations. But the code linked by @joboet leads to a coherence error; you can't get around that just by adding type annotations, so it's "major" breakage and therefore not allowed by Rust's stability guarantees.
The only situations where implementing a trait is a major breaking change are:
- Blanket impls over a trait:
impl<T: SomeTrait> OtherTrait for T {}
. - Impls on
#[fundamental]
types:impl<T: SomeTrait> OtherTrait for U {}
, whereU
is a#[fundametal]
type like&T
,&mut T
, orBox<T>
. - Implementations of
#[fundamental]
traits (currentlyFn
,FnMut
,FnOnce
, andSized
).
I think the check for trait
impl
s on the type alias needs to be more restrictive, otherwise adding a trait implementation will become a breaking change: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=df54044199bc21c63d2565bad97072cb
While we didn't have a test for it, this is already prevented if you actually use two separate crates:
error[E0119]: conflicting implementations of trait `OtherTrait` for type `Alias`
--> $DIR/coherence_cross_crate.rs:21:1
|
LL | impl OtherTrait for Alias {}
| ------------------------- first implementation here
LL | impl OtherTrait for i32 {}
| ^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `Alias`
|
= note: upstream crates may add a new impl of trait `coherence_cross_crate_trait_decl::SomeTrait` for type `i32` in future versions
I added the test in #105895 so we don't accidentally regress this. I'll mark the comments about this as resolved, to keep the discussion here manageable.
@rfcbot fcp merge
This has been a long-time coming. Let's Do This!
Team member @nikomatsakis has proposed to merge this. The next step is review by the rest of the tagged team members:
Concerns:
-
- docs (#63063 (comment))
- function-defining-uses (#63063 (comment))
Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!
cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
See this document for info about what commands tagged team members can give me.
@rfcbot fcp concern
In the lang meeting, @dtolnay identified this example. The idea is that we don't want to permit argument types in an impl to be considered a defining use:
Can we add a negative test ensuring this continues not to compile?
The idea here is not that this could never compile, but that we want to know if it does, because it would affect a pattern people use in practice.
Also, my initial inclination is that it should not compile, because: if you are outside the module of the TAIT, then the impl no longer appears to have the same signature as the trait. We eventually want the impl signatures to be visible to callers, so this would make callers give errors if using the impl signature (but not the trait). Not good.
Apologies I've fallen behind on the current state of things, but I was surprised by the "usage in trait impls" section, and wanted to do some experimentation. Is the following error intentional?
#![feature(type_alias_impl_trait)]
type Closure = impl FnOnce() -> i32;
fn foo(mut x: i32) -> Closure {
move || {
x += 1;
x
}
}
impl core::fmt::Debug for Closure {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
panic!()
}
}
fails with
error[[E0117]](https://doc.rust-lang.org/nightly/error-index.html#E0117): only traits defined in the current crate can be implemented for arbitrary types
--> src/lib.rs:12:1
|
12 | impl core::fmt::Debug for Closure {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^-------
| | |
| | `Closure` is not defined in the current crate
| impl doesn't use only types from inside the current crate
|
= note: define and implement a trait or new type instead
For more information about this error, try `rustc --explain E0117`.
It's fine with me that it not work yet, but I'm surprised at the error message ("Closure
is not defined in the current crate").
It's fine with me that it not work yet
@cramertj I wouldn't expect that to ever work for the same reason that the following doesn't work, without impl Trait
:
type Foo = i32;
impl core::fmt::Debug for Foo {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
panic!()
}
}
The error message is only slightly different here ("i32
is not defined in the current crate" (referring to the target of the type alias rather than the type alias itself)). I agree that it could be clearer, but the error itself is correct: if all the compiler knows about a type alias is a trait that it implements, it must conservatively reject such impls for the sake of coherence. Am I misunderstanding anything?
[I]f all the compiler knows about a type alias is a trait that it implements, it must conservatively reject such impls for the sake of coherence. Am I misunderstanding anything?
But the compiler does (and must) know the concrete type behind the impl Trait
and could verify that it is indeed a local type. I'm not sure if the compiler considers closure types local or not, but the following less ambiguous example doesn't compile either. Should it? (Never mind the fact that you could just impl Debug directly for S...)
#![feature(type_alias_impl_trait)]
type X = impl core::fmt::Debug;
struct S;
fn foo() -> X { S }
impl core::fmt::Debug for X {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
panic!()
}
}
Some examples of #63063 (comment) in real-world code:
-
ndarray
relies on an unnameable type to make trait methods that can only be implemented by ndarray: https://github.com/rust-ndarray/ndarray-stats/blob/0.5.1/src/lib.rs#L69-L99 -
core
does this too, in factError
downcasting would become unsound if impl Trait lets anyone name this type: https://github.com/rust-lang/rust/blob/1.66.0/library/core/src/error.rs#L87-L99 + https://github.com/rust-lang/rust/blob/1.66.0/library/core/src/error.rs#L206-L212.
@nikomatsakis mentions not allowing usage within an impl
to be considered the "defining use" for a TAIT. Surely though we should make a different restriction: a TAIT may not resolve to a "public item in a private module" from a foreign crate (and possibly also not from the same crate if public).
Edit: it appears a slightly different restriction would be better: TAIT should resolve to a public type, or one that is inherently unnameable (like a closure) — in particular, a negative restriction is problematic as pointed out below.
Why? This, and the examples @dtolnay gives, are all similar to the "sealed trait" pattern, which is quite widely used. I believe this pattern emerged as a hack to get around rust's "private type in public interface" error, but it is now widely enough used that it must be supported: effectively "public type in a private module" is a new type of privacy (like pub(crate)
but different). Uses rely on "public private" items not being nameable outside of the defining crate; TAIT should not offer a way around this.
At the same time, one of the goals of TAIT is to allow naming unnameable types like closures, so the above should be noted as an explicit exception.
@cramertj's example could be re-worded as a question: is a closure a locally-defined type, and can we write impls for it?
My hunch is that the answers are yes and yes no, but see no reason this needs to be the case (or this behaviour could be enabled later).
@jdahlstrom's example still looks like a bug in the impl of type_alias_impl_trait
, unless it is explicitly decided that a TAIT symbol may not be an impl target.
Edit: since TAIT resolves an opaque type, probably it should be impossible to write impls for that type (at least without a new RFC).
But the compiler does (and must) know the concrete type behind the
impl Trait
and could verify that it is indeed a local type. I'm not sure if the compiler considers closure types local or not, but the following less ambiguous example doesn't compile either. Should it? (Never mind the fact that you could just impl Debug directly for S...)
Without a Debug
impl for S
this will not compile with your Debug for X
impl removed.
But this is actually expected. Your X
may resolve to u32
, which already has a Debug
impl.
I don't think TAIT should have a "can't resolve to a pub type in private mod" restriction.
- Currently visibility affects only whether you can name an item. It doesn't change in any way how it interacts with the type system. This restriction would be the first thing that breaks that.
- RPITs can resolve to pub types in private mods, and we can't change it because it's stable. This would be an inconsistency between RPIT and TAIT.
You can still make sealed traits with a pub trait in private mod, which is what the Rust API guidelines recommend. This doesn't get affected by TAIT.
Surely though we should make a different restriction: a TAIT may not resolve to a "public item in a private module" from a foreign crate (and possibly also not from the same crate if public).
I don't think this is something the type system should or can care about (especially since with sufficiently complex associated type projections this check is effectively undoable without holes).
Also, as you can still have values of these types, so they are not supposed to be unusable, just not directly referenceable. The change TAIT does is that it will allow you access to trait impls for your private-in-public type. So if you implement Default
for it, TAITs allow you to generate values of it. I guess private-in-public types should not have trait impls with non-self methods on them either?
Niko wrote in #63063 (comment):
Can we add a negative test ensuring this continues not to compile?
this is now being tested in https://github.com/rust-lang/rust/pull/105895/files
Without a Debug impl for S this will not compile with your Debug for X impl removed.
And fn foo() -> X { S }
is used to resolve X = S
, so this is the Debug
impl for S
.
You can still make sealed traits with a pub trait in private mod
Sure, but this feels like leaving the issue for later: what if we a "trait alias ..." feature in the future?
Sealed traits and the other "pub type in priv crates" always felt like a hack. I guess we now have to invent convoluted rules because we never created proper rules around this earlier?
And
fn foo() -> X { S }
is used to resolveX = S
, so this is theDebug
impl forS
.
that's not how these impls work. They do not reveal the hidden type they implement a trait for.
especially since with sufficiently complex associated type projections this check is effectively undoable without holes
If there is already another way to name this type from another crate then it already isn't a "sealed type", so my rule only needs slight re-wording.
@oli-obk lots of short comments aren't great for readability later?
Since this is a big feature, and it changed significantly over the last year, I wrote up a doc explaining the current status of the feature: https://hackmd.io/tB3V8MP5S66zHIWTUdRvQw
I know this is a very late time in the process to ask this question, but: shouldn't that summary document have a "motivation / rationale" section? What is even is the motivation for TAIT?
Because in all the discussions around TAIT I've seen, it seems that the number of use-cases is fairly small and specific (the big one is enabling async traits AFAIK), and TAIT is a very general feature, maybe more general than it needs to be for the use cases. It certainly feels like some aspects of that feature aren't strictly needed, but aren't being worked on because they're cool and scratch the type theory itch. Eg, from the explainer:
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.
Given that TAIT seems to have a lot of corner cases, is it too late to consider designing a simple RPIT-based feature instead?
because they're cool and scratch the type theory itch. Eg, from the explainer:
I should amend that. Just like projections, you cannot prevent TAITs from ending up in impl headers, as they may be hidden behind (possibly generic) projections.
the original motivation for a TAIT-like syntax, in fact, dates back to the introduction of RPIT in 2015 or so. The idea was that it was ok to have a rather limited shorthand like RPIT since we will "quickly thereafter" add an explicit syntax. In particular, the RPIT shorthand uses heuristics to ecide which types/lifetimes it captures and those don't always line up with what you want. We also decide whether to desugar impl Trait
to a generic parameter or an opaque type based on where it appears, and that rule isn't always what you want.
It's certainly true most use cases boil down to RPITIT (return-position impl trait in traits), and I expect us to stabilize that feature soon, but it adds some complications we haven't fully resolved yet. Moreover, even once that happens, it would have the same limitations as RPIT, in that it makes some heuristic decisions and there isn't an "explicit form" that you can fall back to if those don't fit.
The impl trait explainer covers a lot of general background and motivation. That said, it probably doesn't cover as much of this background as it could. My personal goal is that impl Trait
notation works just about anywhere, and I hope we can reach that by end-of-year. This refactoring puts us much closer.
@rfcbot fcp resolve
🔔 This is now entering its final comment period, as per the review above. 🔔
Was the unresolved question of what constitutes a defining use resolved? I couldn't find an answer anywhere in the above discussion or RFC.
Huge thanks to all involved for your work to push this to the FCP for stabilization.
Just today I went down a rabbit hole that ended with the realization that what I wanted to do just wasn't going to work without TAIT (or lots of unsafe
trickery).
It takes a lot of doing to push this kind of thing over the line. Please know that you and your efforts are very much appreciated.