rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
99.05k stars 12.8k forks source link

Tracking issue for `impl Trait` (RFC 1522, RFC 1951, RFC 2071) #34511

Closed aturon closed 5 years ago

aturon commented 8 years ago

NEW TRACKING ISSUE = https://github.com/rust-lang/rust/issues/63066

Implementation status

The basic feature as specified in RFC 1522 is implemented, however there have been revisions that are still in need of work:

RFCs

There have been a number of RFCs regarding impl trait, all of which are tracked by this central tracking issue.

Unresolved questions

The implementation has raised a number of interesting questions as well:

pthariensflame commented 8 years ago

@aturon Can we actually put the RFC in the repository? (@mbrubeck commented there that this was a problem.)

aturon commented 8 years ago

Done.

eddyb commented 8 years ago

First attempt at implementation is #35091 (second, if you count my branch from last year).

One problem I ran into is with lifetimes. Type inference likes to put region variables everywhere and without any region-checking changes, those variables don't infer to anything other than local scopes. However, the concrete type must be exportable, so I restricted it to 'static and explicitly named early-bound lifetime parameters, but it's never any of those if any function is involved - even a string literal doesn't infer to 'static, it's pretty much completely useless.

One thing I thought of, that would have 0 impact on region-checking itself, is to erase lifetimes:

That last point about auto trait leakage is my only worry, everything else seems straight-forward. It's not entirely clear at this point how much of region-checking we can reuse as-is. Hopefully all.

cc @rust-lang/lang

arielb1 commented 8 years ago

@eddyb

But lifetimes are important with impl Trait - e.g.

fn get_debug_str(s: &str) -> impl fmt::Debug {
    s
}

fn get_debug_string(s: &str) -> impl fmt::Debug {
    s.to_string()
}

fn good(s: &str) -> Box<fmt::Debug+'static> {
    // if this does not compile, that would be quite annoying
    Box::new(get_debug_string())
}

fn bad(s: &str) -> Box<fmt::Debug+'static> {
    // if this *does* compile, we have a problem
    Box::new(get_debug_str())
}

I mentioned that several times in the RFC threads

arielb1 commented 8 years ago

trait-object-less version:

fn as_debug(s: &str) -> impl fmt::Debug;

fn example() {
    let mut s = String::new("hello");
    let debug = as_debug(&s);
    s.truncate(0);
    println!("{:?}", debug);
}

This is either UB or not depending on the definition of as_debug.

eddyb commented 8 years ago

@arielb1 Ah, right, I forgot that one of the reasons I did what I did was to only capture lifetime parameters, not anonymous late-bound ones, except it doesn't really work.

eddyb commented 8 years ago

@arielb1 Do we have a strict outlives relation we can put between lifetimes found in the concrete type pre-erasure and late-bound lifetimes in the signature? Otherwise, it might not be a bad idea to just look at lifetime relationships and insta-fail any direct or indirect 'a outlives 'b where 'a is anything other than 'static or a lifetime parameter and 'b appears in the concrete type of an impl Trait.

nikomatsakis commented 8 years ago

Sorry for taking a while to write back here. So I've been thinking about this problem. My feeling is that we do, ultimately, have to (and want to) extend regionck with a new kind of constraint -- I'll call it an \in constraint, because it allows you to say something like '0 \in {'a, 'b, 'c}, meaning that the region used for '0 must be either 'a, 'b, or 'c. I'm not sure of the best way to integrate this into solving itself -- certainly if the \in set is a singleton set, it's just an equate relation (which we don't currently have as a first-class thing, but which can be composed out of two bounds), but otherwise it makes things complicated.

This all relates to my desire to make the set of region constraints more expressive than what we have today. Certainly one could compose a \in constraint out of OR and == constraints. But of course more expressive constraints are harder to solve and \in is no different.

Anyway, let me just lay out a bit of my thinking here. Let's work with this example:

pub fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {...}

I think the most accurate desugaring for a impl Trait is probably a new type:

pub struct FooReturn<'a, 'b> {
    field: XXX // for some suitable type XXX
}

impl<'a,'b> Iterator for FooReturn<'a,'b> {
    type Item = <XXX as Iterator>::Item;
}

Now the impl Iterator<Item=u32> in foo should behave the same as FooReturn<'a,'b> would behave. It's not a perfect match though. One difference, for example, is variance, as eddyb brought up -- I am assuming we will make impl Foo-like types invariant over the type parameters of foo. The auto trait behavior works out, however. (Another area where the match might not be ideal is if we ever add the ability to "pierce" the impl Iterator abstraction, so that code "inside" the abstraction knows the precise type -- then it would sort of have an implicit "unwrap" operation taking place.)

In some ways a better match is to consider a kind of synthetic trait:

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    type Type = XXX;
}

Now we could consider the impl Iterator type to be like <() as FooReturn<'a,'b>>::Type. This is also not a perfect match, because we would ordinarily normalize it away. You might imagine using specialization to prevent that though:

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    default type Type = XXX; // can't really be specialized, but wev
}

In this case, <() as FooReturn<'a,'b>>::Type would not normalize, and we have a much closer match. The variance, in particular, behaves right; if we ever wanted to have some type that are "inside" the abstraction, they would be the same but they are allowed to normalize. However, there is a catch: the auto trait stuff doesn't quite work. (We may want to consider harmonizing things here, actually.)

Anyway, my point in exploring these potential desugarings is not to suggest that we implement "impl Trait" as an actual desugaring (though it might be nice...) but to give an intuition for our job. I think that the second desugaring -- in terms of projections -- is a pretty helpful one for guiding us forward.

One place that this projection desugaring is a really useful guide is the "outlives" relation. If we wanted to check whether <() as FooReturn<'a,'b>>::Type: 'x, RFC 1214 tells us that we can prove this so long as 'a: 'x and 'b: 'x holds. This is I think how we want to handle things for impl trait as well.

At trans time, and for auto-traits, we will have to know what XXX is, of course. The basic idea here, I assume, is to create a type variable for XXX and check that the actual values which are returned can all be unified with XXX. That type variable should, in theory, tell us our answer. But of course the problem is that this type variable may refer to a lot of regions which are not in scope in the fn signature -- e.g., the regions of the fn body. (This same problem does not occur with types; even though, technically, you could put e.g. a struct declaration in the fn body and it would be unnameable, that's a kind of artificial restriction -- one could just as well move the struct outside the fn.)

If you look both at the struct desugaring or the impl, there is an (implicit in the lexical structure of Rust) restriction that XXX can only name either 'static or lifetimes like 'a and 'b, which appear in the function signature. That is the thing we are not modeling here. I'm not sure the best way to do it -- some type inference schemes have a more direct representation of scoping, and I've always wanted to add that to Rust, to help us with closures. But let's think about smaller deltas first I guess.

This is where the \in constraint comes from. One can imagine adding a type-check rule that (basically) FR(XXX) \subset {'a, 'b} -- meaning that the "free regions" appearing in XXX can only be 'a and 'b. This would wind up translating to \in requirements for the various regions that appear in XXX.

Let's look at an actual example:

fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Here, the type if condition is true would be something like Cloned<SliceIter<'a, i32>>. But if condition is false, we would want Cloned<SliceIter<'b, i32>>. Of course in both cases we would wind up with something like (using numbers for type/region variables):

Cloned<SliceIter<'0, i32>> <: 0
'a: '0 // because the source is x.iter()
Cloned<SliceIter<'1, i32>> <: 0
'b: '1 // because the source is y.iter()

If we then instantiate the variable 0 to Cloned<SliceIter<'2, i32>>, we have '0: '2 and '1: '2, or a total set of region relations like:

'a: '0
'0: '2
'b: '1
'1: '2
'2: 'body // the lifetime of the fn body

So what value should we use for '2? We have also the additional constraint that '2 in {'a, 'b}. With the fn as written, I think we would have to report an error, since neither 'a nor 'b is a correct choice. Interestingly, though, if we added the constraint 'a: 'b, then there would be a correct value ('b).

Note that if we just run the normal algorithm, we would wind up with '2 being 'body. I'm not sure how to handle the \in relations except for exhaustive search (though I can imagine some special cases).

OK, that's as far as I've gotten. =)

nikomatsakis commented 8 years ago

On the PR #35091, @arielb1 wrote:

I don't like the "capture all lifetimes in the impl trait" approach and would prefer something more like lifetime elision.

I thought it would make more sense to discuss here. @arielb1, can you elaborate more on what you have in mind? In terms of the analogies I made above, I guess you are fundamentally talking about "pruning" the set of lifetimes that would appear either as parameters on the newtype or in the projection (i.e., <() as FooReturn<'a>>::Type instead of <() as FooReturn<'a,'b>>::Type or something?

I don't think that the lifetime elision rules as they exist would be a good guide in this respect: if we just picked the lifetime of &self to include only, then we wouldn't necessarily be able to include the type parameters from the Self struct, nor type parameters from the method, since they may have WF conditions that require us to name some of the other lifetimes.

Anyway, it'd be great to see some examples that illustrate the rules you have in mind, and perhaps any advantages thereof. :) (Also, I guess we would need some syntax to override the choice.) All other things being equal, if we can avoid having to pick from N lifetimes, I'd prefer that.

petrochenkov commented 8 years ago

I haven't seen interactions of impl Trait with privacy discussed anywhere. Now fn f() -> impl Trait can return a private type S: Trait similarly to trait objects fn f() -> Box<Trait>. I.e. objects of private types can walk freely outside of their module in anonymized form. This seems reasonable and desirable - the type itself is an implementation detail, only its interface, available through a public trait Trait is public. However there's one difference between trait objects and impl Trait. With trait objects alone all trait methods of private types can get internal linkage, they will still be callable through function pointers. With impl Traits trait methods of private types are directly callable from other translation units. The algorithm doing "internalization" of symbols will have to try harder to internalize methods only for types not anonymized with impl Trait, or to be very pessimistic.

arielb1 commented 8 years ago

@nikomatsakis

The "explicit" way to write foo would be

fn foo<'a: 'c,'b: 'c,'c>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> + 'c {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Here there is no question about the lifetime bound. Obviously, having to write the lifetime bound each time would be quite repetitive. However, the way we deal with that kind of repetition is generally through lifetime elision. In the case of foo, elision would fail and force the programmer to explicitly specify lifetimes.

I am opposed to adding explicitness-sensitive lifetime elision as @eddyb did only in the specific case of impl Trait and not otherwise.

nikomatsakis commented 8 years ago

@arielb1 hmm, I'm not 100% sure how to think about this proposed syntax in terms of the "desugarings" that I discussed. It allows you to specify what appears to be a lifetime bound, but the thing we are trying to infer is mostly what lifetimes appear in the hidden type. Does this suggest that at most one lifetime could be "hidden" (and that it would have to be specified exactly?)

It seems like it's not always the case that a "single lifetime parameter" suffices:

fn foo<'a, 'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    x.iter().chain(y).cloned()
}

In this case, the hidden iterator type refers to both 'a and 'b (although it is variant in both of them; but I guess we could come up with an example that is invariant).

nikomatsakis commented 8 years ago

So @aturon and I discussed this issue somewhat and I wanted to share. There are really a couple of orthogonal questions here and I want to separate them out. The first question is "what type/lifetime parameters can potentially be used in the hidden type?" In terms of the (quasi-)desugaring into a default type, this comes down to "what type parameters appear on the trait we introduce". So, for example, if this function:

fn foo<'a, 'b, T>() -> impl Trait { ... }

would get desugared to something like:

fn foo<'a, 'b, T>() -> <() as Foo<...>>::Type { ... }
trait Foo<...> {
  type Type: Trait;
}
impl<...> Foo<...> for () {
  default type Type = /* inferred */;
}

then this question comes down to "what type parameters appear on the trait Foo and its impl"? Basically, the ... here. Clearly this include include the set of type parameters that appear are used by Trait itself, but what additional type parameters? (As I noted before, this desugaring is 100% faithful except for the leakage of auto traits, and I would argue that we should leak auto traits also for specializable impls.)

The default answer we've been using is "all of them", so here ... would be 'a, 'b, T (along with any anonymous parameters that may appear). This may be a reasonable default, but it's not necessarily the best default. (As @arielb1 pointed out.)

This has an effect on the outlives relation, since, in order to determine that <() as Foo<...>>::Type (referring to some particular, opaque instantiation of impl Trait) outlives 'x, we effectively must show that ...: 'x (that is, every lifetime and type parameter).

This is why I say it is not enough to consider lifetime parameters: imagine that we have some call to foo like foo::<'a0, 'b0, &'c0 i32>. This implies that all three lifetimes, '[abc]0, must outlive 'x -- in other words, so long as the return value is in use, this will prolog the loans of all data given into the function. But, as @arielb1 poitned out, elision suggests that this will usually be longer than necessary.

So I imagine that what we need is:

@aturon spitballed something like impl<...> Trait as the explicit syntax, which seems reasonable. Therefore, one could write:

fn foo<'a, 'b, T>(...) -> impl<T> Trait { }

to indicate that the hidden type does not in fact refer to 'a or 'b but only T. Or one might write impl<'a> Trait to indicate that neither 'b nor T are captured.

As for the defaults, it seems like having more data would be pretty useful -- but the general logic of elision suggests that we would do well to capture all the parameters named in the type of self, when applicable. E.g., if you have fn foo<'a,'b>(&'a self, v: &'b [u8]) and the type is Bar<'c, X>, then the type of self would be &'a Bar<'c, X> and hence we would capture 'a, 'c, and X by default, but not 'b.


Another related note is what the meaning of a lifetime bound is. I think that sound lifetime bounds have an existing meaning that should not be changed: if we write impl (Trait+'a) that means that the hidden type T outlives 'a. Similarly one can write impl (Trait+'static) to indicate that there are no borrowed pointers present (even if some lifetimes are captured). When inferring the hidden type T, this would imply a lifetime bound like $T: 'static, where $T is the inference variable we create for the hidden type. This would be handled in the usual way. From a caller's perspective, where the hidden type is, well, hidden, the 'static bound would allow us to conclude that impl (Trait+'static) outlives 'static even if there are lifetime parameters captured.

Here it just behaves exactly as the desugaring would behave:

fn foo<'a, 'b, T>() -> <() as Foo<'a, 'b, 'T>>::Type { ... }
trait Foo<'a, 'b, T> {
  type Type: Trait + 'static; // <-- note the `'static` bound appears here
}
impl<'a, 'b, T> Foo<...> for () {
  default type Type = /* something that doesn't reference `'a`, `'b`, or `T` */;
}

All of this is orthogonal from inference. We still want (I think) to add the notion of a "choose from" constraint and modify inference with some heuristics and, possibly, exhaustive search (the experience from RFC 1214 suggests that heuristics with a conservative fallback can actually get us very far; I'm not aware of people running into limitations in this respect, though there is probably an issue somewhere). Certainly, adding lifetime bounds like 'static or 'a` may influence inference, and thus be helpful, but that is not a perfect solution: for one thing, they are visible to the caller and become part of the API, which may not be desired.

arielb1 commented 8 years ago

Possible options:

Explicit lifetime bound with output parameter elision

Like trait objects today, impl Trait objects have a single lifetime bound parameter, which is inferred using the elision rules.

Disadvantage: unergonomic Advantage: clear

Explicit lifetime bounds with "all generic" elision

Like trait objects today, impl Trait objects have a single lifetime bound parameter.

However, elision creates a new early-bound parameters that outlives all explicit parameters:

fn foo<T>(&T) -> impl Foo
-->
fn foo<'total, T: 'total>(&T) -> impl Foo + 'total

Disadvantage: adds an early-bound parameter

more.

Boscop commented 8 years ago

I ran into this issue with impl Trait +'a and borrowing: https://github.com/rust-lang/rust/issues/37790

IanWhitney commented 7 years ago

If I'm understanding this change correctly (and the chance of that is probably low!), then I think this playground code should work:

https://play.rust-lang.org/?gist=496ec05e6fa9d3a761df09c95297aa2a&version=nightly&backtrace=0

Both ThingOne and ThingTwo implement the Thing trait. build says it will return something that implements Thing, which it does. Yet it does not compile. So I'm clearly misunderstanding something.

eddyb commented 7 years ago

That "something" must have a type, but in your case you have two conflicting types. @nikomatsakis has previously suggested making this work in general by creating e.g. ThingOne | ThingTwo as type mismatches appear.

WiSaGaN commented 7 years ago

@eddyb could you elaborate on ThingOne | ThingTwo? Don't you need to have Box if we only know the type at run-time? Or is it a kind of enum?

eddyb commented 7 years ago

Yeah it could be an ad-hoc enum-like type that delegated trait method calls, where possible, to its variants.

glaebhoerl commented 7 years ago

I've wanted that kind of thing before too. The anonymous enums RFC: https://github.com/rust-lang/rfcs/pull/1154

eddyb commented 7 years ago

It's a rare case of something that works better if it's inference-driven, because if you only create these types on a mismatch, the variants are different (which is a problem with the generalized form). Also you can get something out of not having pattern-matching (except in obviously disjoint cases?). But IMO delegation sugar would "just work" in all relevant cases, even if you manage to get a T | T.

glaebhoerl commented 7 years ago

Could you spell out the other, implicit halves of those sentences? I don't understand most of it, and suspect I'm missing some context. Were you implicitly responding to the problems with union types? That RFC is simply anonymous enums, not union types - (T|T) would be exactly as problematic as Result<T, T>.

eddyb commented 7 years ago

Oh, nevermind, I got the proposals confused (also stuck on mobile until I sort out my failing HDD so apologies for sounding like on Twitter).

I find (positional, i.e T|U != U|T) anonymous enums intriguing, and I believe they could be experimented with in a library if we had variadic generics (you can side-step this by using hlist) and const generics (ditto, with peano numbers).

But, at the same time, if we had language support for something, it'd be union types, not anonymous enums. E.g. not Result but error types (to bypass the tedium of named wrappers for them).

kud1ing commented 7 years ago

I am not sure whether this is the righ place to ask, but why is a keyword like impl needed? I could not find a discussion (could be my fault).

If a function returns impl Trait, its body can return values of any type that implements Trait

Since

fn bar(a: &Foo) {
  ...
}

means "accept a reference to a type that implements trait Foo" i would expect

fn bar() -> Foo {
  ...
}

to mean "return a type that implements trait Foo". Is this impossible?

Nemo157 commented 7 years ago

@kud1ing the reason is to not remove the possibility of having a function that returns the dynamically sized type Trait if support for dynamically sized return values is added in the future. Currently Trait is already a valid DST, it's just not possible to return a DST so you need to box it to make it a sized type.

EDIT: There is some discussion about this on the linked RFC thread.

Enet4 commented 7 years ago

Well, for one, regardless of whether dynamically sized return values will be added, I prefer the current syntax. Unlike what happens with trait objects, this isn't type erasure, and any coincidences like "parameter f: &Foo takes something that impls Foo, whereas this returns something that impls Foo" could be misleading.

Nercury commented 7 years ago

I gathered from RFC discussion that right now impl is a placeholder implementation, and no impl is very much desired. Is there any reason for not wanting an impl Trait if the return value is not DST?

nikomatsakis commented 7 years ago

I think the current impl technique for handling "auto trait leakage" is problematic. We should instead enforce a DAG ordering so that if you define a fn fn foo() -> impl Iterator, and you have a caller fn bar() { ... foo() ... }, then we have to type-check foo() before bar() (so that we know what the hidden type is). If a cycle results, we'd report an error. This is a conservative stance -- we can probably do better -- but I think the current technique, where we collect auto-trait obligations and check them at the end, does not work in general. For example, it would not work well with specialization.

(Another possibility that might be more permissive than requiring a strict DAG is to type-check both fns "together" to some extent. I think that is something to consider only after we have re-archicted the trait system impl a bit.)

nikomatsakis commented 7 years ago

@Nercury I don't understand. Are you asking if there are reasons to not want fn foo() -> Trait to mean -> impl Trait?

Nercury commented 7 years ago

@nikomatsakis Yes, I was asking precisely that, sorry for convulted language :). I thought that doing this without impl keyword would be simpler, because this behavior is exactly what one would expect (when a concrete type is returned in place of trait return type). However, I might be missing something, that's why I was asking.

rpjohnst commented 7 years ago

The difference is that functions returning impl Trait always return the same type- it's basically return type inference. IIUC, functions returning just Trait would be able to return any implementation of that trait dynamically, but the caller would need to be prepared to allocate space for the return value via something like box foo().

Ixrec commented 7 years ago

@Nercury The simple reason is that the -> Trait syntax already has a meaning, so we have to use something else for this feature.

I've actually seen people expect both kinds of behavior by default, and this sort of confusion comes up often enough I'd honestly rather that fn foo() -> Trait not mean anything (or be a warning by default) and there were explicit keywords for both the "some type known at compile time that I get to choose but the caller doesn't see" case and the "trait object that could be dynamically dispatching to any type implementing Trait" case, e.g. fn foo() -> impl Trait vs fn foo() -> dyn Trait. But obviously those ships have sailed.

NeoLegends commented 7 years ago

Why doesn't the compiler generate an enum that holds all the different return types of the function, implements the trait passing though the arguments to each variant, and returns that instead?

That would bypass the only one return type allowed-rule.

Ixrec commented 7 years ago

@NeoLegends Doing this manually is fairly common, and some sugar for it might be nice and has been proposed in the past, but it's a third completely different set of semantics from returning impl Trait or a trait object, so it's not really relevant to this discussion.

NeoLegends commented 7 years ago

@Ixrec Yeah I know this is being done manually, but the real use case of the anonymous enums as compiler generated return types is types that you cannot spell out, like long chains of iterator or future adaptors.

How is this different semantics? Anonymous enums (as far as the compiler generates them, not as per the anonymous enums RFC) as return values only really make sense if there is a common API like a trait that abstracts away the different variants. I'm suggesting a feature that still looks like and behaves like the regular impl Trait, just with the one-type-limit removed through a compiler generated enum the consumer of the API will never see directly. The consumer should always only see 'impl Trait'.

rpjohnst commented 7 years ago

Anonymous, auto-generated enums gives impl Trait a hidden cost that's easy to miss, so that's something to consider.

glaebhoerl commented 7 years ago

I suspect the "auto enum pass-through" thing only makes sense for object-safe traits. Is the same thing true of impl Trait itself?

Nercury commented 7 years ago

@rpjohnst Unless this the actual method variant is in crate metadata and monomorphised at the call site. Of course, this requires that change from one variant to another does not break the caller. And this might be too magical.

nikomatsakis commented 7 years ago

@glaebhoerl

I suspect the "auto enum pass-through" thing only makes sense for object-safe traits. Is the same thing true of impl Trait itself?

this is an interesting point! I have been debating what is the right way to "desugar" impl trait, and was actually on the verge of suggesting that maybe we wanted to think of it more as a "struct with a private field" as opposed to the "abstract type projection" interpretation. However, that seems to imply something much like generalized newtype deriving, which of course was famously found to be unsound in Haskell when combined with type families. I confess to not having a full understanding of this unsoundness "in cache" but it seems like we would have to be very cautious here whenever we want to automatically generate an implementation of a trait for some type F<T> from an impl for T.

arielb1 commented 7 years ago

@nikomatsakis

The problem is, in Rust terms

trait Foo {
    type Output;
    fn get() -> Self::Output;
}

fn foo() -> impl Foo {
    // ...
    // what is the type of return_type::get?
}
glaebhoerl commented 7 years ago

The tl;dr is that generalized newtype deriving was (and is) implemented by simply transmuteing the vtable -- after all, a vtable consists of functions on the type, and a type and its newtype have the same representation, so should be fine, right? But it breaks if those functions also use types which are determined by type-level branching on the identity (rather than representation) of the given type -- e.g., using type functions or associated types (or in Haskell, GADTs). Because there's no guarantee that the representations of those types are also compatible.

Note that this problem is only possible because of the use of unsafe transmute. If it instead just generated the boring boilerplate code to wrap/unwrap the newtype everywhere and dispatch every method to its implementation from the base type (like some of the automatic delegation proposals for Rust IIRC?), then the worst possible outcome would be a type error or maybe an ICE. After all, by construction, if you do not use unsafe code you cannot have an unsafe outcome. Likewise, if we generated code for some kind of "automatic enum passthrough", but didn't use any unsafe primitives to do so, there wouldn't be any danger.

(I'm not sure whether or how this relates to my original question of whether the traits used with impl Trait, and/or automatic enum passthrough, by necessity would have to be object-safe, though?)

jaredr commented 7 years ago

@rpjohnst One could make the enum case opt-in to mark the cost:

fn foo() -> enum impl Trait { ... }

That's almost certainly food for a different RFC though.

nikomatsakis commented 7 years ago

@glaebhoerl yeah I spent some time digging into the issue and felt fairly convinced it would not be a problem here, at least.

aldanor commented 7 years ago

Apologies if it's something obvious but I'm trying to understand the reasons why impl Trait can't appear in return types of trait methods, or whether it makes sense at all in the first place? E.g.:

trait IterInto {
    type Output;
    fn iter_into(&self) -> impl Iterator<Item=impl Into<Self::Output>>;
}
pthariensflame commented 7 years ago

@aldanor It totally makes sense, and AFAIK the intention is to make that work, but it hasn't been implemented yet.

eddyb commented 7 years ago

It sort of makes sense, but it's not same underlying feature (this has been discussed a lot btw):

// What that trait would desugar into:
trait IterInto {
    type Output;
    type X: Into<Self::Output>;
    type Y: Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y;
}

// What an implementation would desugar into:
impl InterInto for FooList {
    type Output = Foo;
    // These could potentially be left unspecified for
    // a similar effect, if we want to allow that.
    type X = impl Into<Foo>;
    type Y = impl Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y {...}
}

Specifically, impl Trait in the impl Trait for Type associated types' RHSes would be similar to the feature implemented today, in that it can't be desugared to stable Rust, whereas in the trait it can be.

jonhoo commented 7 years ago

I know this is probably both too late, and mostly bikeshedding, but has it been documented anywhere why the keyword impl was introduced? It seems to me like we already have a way in current Rust code to say "the compiler figures out what type goes here", namely _. Could we not re-use this here to give the syntax:

fn foo() -> _ as Iterator<Item=u8> {}
eddyb commented 7 years ago

@jonhoo That's not what the feature does, the type is not the one returned from the function, but rather a "semantic wrapper" that hides everything except the chosen APIs (and OIBITs because those are a pain).

We could allow some functions to infer types in their signatures by forcing a DAG, but such a feature has never been approved and it's unlikely to ever be added to Rust, as it'd be touching on "global inference".

J-F-Liu commented 7 years ago

Suggest the use of @Trait syntax to replace impl Trait, as mentioned here.

It is easier to extend to other type positions and in composition like Box<@MyTrait> or &@MyTrait.

eddyb commented 7 years ago

@Trait for any T where T: Trait and ~Trait for some T where T: Trait:

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> ~Fn(T) -> V {
    move |x| g(f(x))
}