rust-lang / rust

Empowering everyone to build reliable and efficient software.

Home Page:https://www.rust-lang.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

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?

    • fn foo(x: bool) -> impl Debug {
          if x { return vec!["hi"] }
          Default::default()
      }
      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 a Vec<&'static str>
    • impl Foo can be used for associated types that expect a type that implements Bar, even if Foo and Bar 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:

  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 */ }
  1. 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:

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=8b3fd5bbb3b1e9ebb4d2e4b874b696d3

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:

// There was already an entry for `p`, meaning a generic parameter
// was used twice.
self.tcx.sess.span_err(
span,
&format!(
"defining opaque type use restricts opaque \
type by using the generic parameter `{}` twice",
p,
),
);
return;

While the lifetime parameter error is from wfcheck:

for (_, spans) in seen {
if spans.len() > 1 {
tcx.sess
.struct_span_err(
span,
"non-defining opaque type use \
in defining scope",
)
.span_note(spans, "lifetime used multiple times")
.emit();
}
}

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 {
        ()
    }
}

(playground link)

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);
}

(playground)

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

(playground)

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.

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=5e81b89ae9cd0e686b2dfaed90a51418

@petar-dambovaliev The trait definition needs to specify a bound. The syntax you used goes in the impls.

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=5e81b89ae9cd0e686b2dfaed90a51418

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.

  1. 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
  2. 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::pins 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 .awaiting 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 };
}

(playground)

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.

commented

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?

commented

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 `#[feature(type_alias_impl_trait)]` ) and once I'm confident that we don't have any more future incompatibility issues left, push to move this feature towards stabilization

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.

@ibraheemdev:

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 impls 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 impls on the type alias needs to be more restrictive, otherwise adding a trait impl 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 impls on the type alias needs to be more restrictive, otherwise adding a trait impl 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 {}?

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.

https://rust-lang.github.io/rfcs/1105-api-evolution.html#minor-change-implementing-any-non-fundamental-trait

(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 {}, where U is a #[fundametal] type like &T, &mut T, or Box<T>.
  • Implementations of #[fundamental] traits (currently Fn, FnMut, FnOnce, and Sized).

I think the check for trait impls 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!

Stabilization report in this comment.

Team member @nikomatsakis has proposed to merge this. The next step is review by the rest of the tagged team members:

Concerns:

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:

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=df2d095355a61791ec5a98794a5a4807

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:

@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 resolve X = S, so this is the Debug impl for S.

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?

@oli-obk lots of short comments aren't great for readability later?

to be fair, discussions on tracking issues aren't great ^^ We should probably just move them to zulip

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.

@PoignardAzur

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.