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 1951: Finalize syntax and parameter scoping for `impl Trait`, while expanding it to arguments

aturon opened this issue · comments

This is a tracking issue for the RFC "Finalize syntax and parameter scoping for impl Trait, while expanding it to arguments" (rust-lang/rfcs#1951).

Steps:

Unresolved questions:

  • The precedence rules around impl Trait + 'a need to be nailed down.

Known bugs:

(this list is not necessarily exhaustive)

I would hate to slow up impl Trait (which I am very eager for), but I am disappointed that it does not support generative existentials. Specifically, I could imagine a scenario such as

enum Foo {
   Vec(Vec<u8>),
   Set(HashSet<u8>,
}

fn mkiter(f: Foo) -> impl Iterator<Item=&u8> {
    match f {
      Foo::Vec(ref v) => v.iter(),
      Foo::Set(ref s) => s.iter(),
    }
}

Currently I could do this with a trait object, but that would expose the dynamicness in my API. So instead I would create a new enum to hold the iterator. It looks like with impl Trait as proposed, I still have to create my new enum and implement Iterator on that enum, and then I can return an impl Trait?

@droundy That came up a few times, as creating an "intersection type", in your case it would be something like vec::Iter<u8> | hash_set::Iter<u8>, which would dispatch its methods to whichever type it was constructed from. The big difference from the other proposal with that syntax, "anonymous sum types", is that an intersection type can't be matched, as T | T collapses to T, but as a sum type, you have to distinguish between the two T "variants", so either match doesn't work in some cases orT | T is banned somehow (almost impossible with generics).

I am actually in favor of doing it, for impl Trait or even in more cases, when multiple choices of type exist (if-else, match arms, array elements etc.) and they conflict between themselves.
That is, only code that currently doesn't compile would automatically get T | U types.

If I'm understanding you, @eddyb, this means that this is case won't work in the current proposal, but there is room for a backwards compatible change that will enable automatic generation of sum types in the future?

Yes, it only showed up in related discussion but is not part of any accepted proposal.

Before landing anything here we should make sure we correctly handle (or error on) lifetime elision in impl Trait. See #43396.

The more conservative option is to just ban elision entirely for the time being.

The case @droundy describes has a pretty common equivalent in futures, basically anywhere the Either future is currently used. I often reach for it when futures code branches on a condition (for example, something existing already or having to be created first.

I've certainly encountered the desire for | types in practice. I am a bit wary of them (although it occurs to me that they could be a helpful way to avoid the need to compute LUB for thorny cases), but it'd be a nice way to resolve this problem. I would certainly like for most users to not be forced to be aware of their existence. At least trait authors would presumably have to be, though, since you would probably have to provide something like impl<A,B> Future for (A|B). We could maybe synthesize such impls, but not, I think, in all cases (the restrictions are probably similar-ish to object safety).

@nikomatsakis I think there is a big difference between type-visible intersection types and those that would/could be hidden behind impl Trait. The latter would not require any users to be aware of their existence, and thus (I think) would not introduce this sort of complexity. It would just lift the restriction that a function returning impl Trait must return the same concrete type on each branch, at the cost of runtime checks generated by the compiler to determine which concrete type actually was returned.

We could maybe synthesize such impls, but not, I think, in all cases (the restrictions are probably similar-ish to object safety).

If you allow user matching on such types which is needed for manual impls, then T | T becomes a problem - IMO synthesized impls should be the only thing you can do with these types.

I wrote a little bit about this a while ago, including what implementations could be synthesized automatically, and how trait authors could specify their own :
https://internals.rust-lang.org/t/pre-rfc-anonymous-enum/4806

@droundy

My point is merely that we cannot always synthesize impls. I might be fine with saying "when the traits meets the required criteria, we will permit | types in impl trait", but then this becomes a "silent propery" of the trait that one must be careful not to violate. We have some precedent fro this (object safety), but it's not one of the most loved features of the language (for many reasons...). I've not read @plietar's post yet (or at least not in a while...), but I guess that they enumerated some examples, so I won't bother to do so here. =)

A thought from dyn Trait discussions: what about blanket impls for other traits?

With this + dyn, we have

fn foo(x: &dyn Trait)
fn foo(x: impl Trait)

impl Trait1 for dyn Trait2 { ... }
impl<T:Trait2> Trait1 for T { ... }

Should the last of those also be "argument position" and thus this? (Eventually, at least.)

impl Trait1 for impl Trait2 { ... }

That's sortof logical, and Self can be used to refer to the actual type. But it's also kinda weird to look at. Not that I really want to re-open the bikeshed...

Can this be closed? #34511 is the tracking issue for further development of impl trait/abstract type.

Closing in favor of #34511.