Open Centril opened 5 years ago
@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 https://github.com/rust-lang/rust/pull/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 https://github.com/rust-lang/rust/pull/63096, landed on 2019-07-29, written by @Centril and reviewed by @varkor, 3 ICEs were closed with reproducer tests added.
In https://github.com/rust-lang/rust/pull/63158, landed on 2019-07-31, written by @JohnTitor and reviewed by @Centril, 1 ICE was closed with a reproducer test added.
In https://github.com/rust-lang/rust/pull/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 https://github.com/rust-lang/rustc-guide/pull/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:
1) 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). 2) 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:
1) 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 */ }
2) 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: https://github.com/rust-lang/rust/issues/52843#issuecomment-457578915
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 https://github.com/rust-lang/rust/issues/52843#issue-345582081.
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 = _;
(https://github.com/rust-lang/rfcs/pull/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 }
~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 https://github.com/rust-lang/rust/issues/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
: https://github.com/rust-lang/rust/blob/5574b1df577f737373b42cbf364b6cab2dfa5960/src/librustc_typeck/collect/type_of.rs#L410-L420
While the lifetime parameter error is from wfcheck
:
https://github.com/rust-lang/rust/blob/5574b1df577f737373b42cbf364b6cab2dfa5960/src/librustc_typeck/check/wfcheck.rs#L925-L936
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
(https://github.com/rust-lang/rust/pull/57896#discussion_r396064431) 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 https://github.com/rust-lang/rust/issues/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.
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: https://github.com/rust-lang/rust/pull/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.
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.
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
: https://github.com/rust-lang/rust/pull/110237About 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
Unresolved questions
[x] Exactly what should count as "defining uses" for opaque types?
[x] 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
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.[x] impl traits in consts through const fns are allowed but shouldn't be: https://github.com/rust-lang/rust/issues/87277