Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Multi-Type Return Position Impl Trait (MTRPIT) #3367

Conversation

yoshuawuyts
Copy link
Member

@yoshuawuyts yoshuawuyts commented Jan 5, 2023

Summary

This RFC enables Return Position Impl Trait (RPIT) to work in functions which return more than one type:

// possible already
fn single_iter() -> impl Iterator<Item = i32> {
    1..10 // `std::ops::Range<i32>`
}

// enabled by this RFC
fn multi_iter(x: i32) -> impl Iterator<Item = i32> {
    match x {
        0 => 1..10,                   // `std::ops::Range<i32>`
        _ => vec![5, 10].into_iter(), // `std::vec::IntoIter<i32>`
    }
}

Rendered

@ChayimFriedman2
Copy link

I would prefer an explicit syntax, such as enum impl Trait, as I think doing it implicitly could become a performance footgun.

@yoshuawuyts yoshuawuyts force-pushed the return-position-enum-impl-trait branch from a5447ff to 2ddaa30 Compare January 5, 2023 17:51
@shepmaster
Copy link
Member

Language-level support for delegation/proxies

I'd much rather see this alternative implemented first. Delegation is the number one language-level complaint I have personally.

The lack of delegation is also a missing piece of the puzzle when I am training people who are familiar with classical inheritance-based OOP. Many of these people use inheritance to achieve code reuse1 and ask how they can achieve similar benefits when transitioning to Rust. I have to shrug and vaguely wave my hand towards non-ideal code generation techniques and say "someday I hope Rust gets delegation".

Since the delegation functionality appears to be a prerequisite for the implementation of this RFC, it seems to me that the difference in effort would be the user-level syntax that exposes delegation. I'd propose following that thread first and then seeing how much of this RFC is needed — maybe everyone is happy with delegation + the remaining hand-created implementation and this RFC isn't needed, or perhaps a pattern emerges in the ecosystem that would improve or replace what this RFC proposes.

Footnotes

  1. For better or worse — I have my opinions but this is off topic.

@truppelito
Copy link

This seems to be related to 2414, which I didn't see mentioned after a small cursory glance at this RFC.

I'm very much in favor of this. It is a substantial quality of life improvement: I have run across multiple use cases for this feature and it just seems like something that should "just work", i.e. the compiler can automate for me, instead of having to manually declare one-use enums or having to work with/around the shortcomings of the auto-enums crate.

I would strongly advocate for an x() -> enum impl MyTrait syntax because I do think that it should be clear what the compiler is doing for us. Clarity of code abstractions is, after all, a major priority of the Rust language.


1. Find all return calls in the function
2. Define a new enum with a member for each of the function's return types
3. Implement the traits declared in the `-> impl Trait` bound for the new enum,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you do this for traits that have fn() -> Self? For example, Default doesn't have an obvious delegation.

This seems like a key thing to discuss, particularly as it means that the addition of such methods might be semver-breaking, even in a sealed trait.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you do this for traits that have fn() -> Self? For example, Default doesn't have an obvious delegation.

Well, maybe that's just one of the restrictions that may have to come with this feature.
I think returning Self could work if you're method chaining. For example SomeType::impl_fn().do_something().get_concrete_value();. Though, not being able to use any -> Self methods wouldn't be too big of a deal breaker if it meant we had this feature.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To: @Mark-Simulacrum, @CAD97
CC: @yoshuawuyts, @Ekleog, @glaebhoerl

I thoughts of a really interesting solution regarding traits that have fn() -> Self (or receiver variants).
Basically, calling a method that returns Self is an error because there is we can't know what that type is, we only know what method that value has that we can call. - I believe E0119 and E0411 are both already compiler errors that could possibly represent this.
It could return a simple compiler error like:

/* Human style. */
Can not return ambiguous `Self` for static dispatch type `impl Trait`.


/* Structured style. */
error: ambiguous type `Self` on associated method return type

Regardless of whether this feature would need its own error code or falls under something like E0411 I've decided to make examples of various different situations.

Examples of Theoretical Errors

Attempted type-inference for static-dispatch-type {impl T}.

Erroneous code:

trait Baz {
  fn buzz(&self) {}
}

struct Foo;
impl Baz for Foo {}
struct Bar;
impl Baz for Bar {}

fn some_value() -> impl Baz {
  /* ... */
}

fn main() {
  let foo = some_value(); // error: can not infer single type for `foo` in the set of
                          //        all types that implement `Baz`.
}

To fix the code the compiler may suggest calling the .buzz() method of Baz.
I.e:

trait Baz {
  fn buzz(&self) -> &str { "Buzz!" }
}

struct Foo;
impl Baz for Foo {}
struct Bar;
impl Baz for Bar {}

fn some_value() -> impl Baz {
  /* ... */
}

fn main() {
  let buzz = some_value().buzz(); // this is o.k. because we know we'll
                                  // always get an `&str` from `{impl Baz}::buzz(&self)`.
}

Unresolved type Self for {impl T}::name() -> Self.

Like with the previous error, the crux of the issue is inferencing and assigning a type for Self, which I don't believe is possible.

Erroneous code:

trait Baz {
  fn fizz(&self) -> Self {/* ... */}
}

struct Foo;
impl Baz for Foo {}
struct Bar;
impl Baz for Bar {}

fn some_value() -> impl Baz {/* ... */}

fn main() {
  let fizzed = some_value().fizz(); // error: can not infer the type of `Self` for.
                                    //        local value of type `{impl Baz}`.
}

Explicitness

As suggested, an explicit syntax like enum impl T, which would turn:

fn some_value() -> impl MyTrait {/* ... */}

into:

fn some_value() -> enum impl MyTrait {/* ... */}

It would also be logical to accordingly adjust the "{impl T}" placeholder into "{enum impl T}", but I think the former suffices.
With this explicitness it makes it easy to pinpoint functions that could be potential troublemakers for the kinds of scenarios we can think up. It also allows us to keep the normal -> impl T behavior, and extend that functionality to static dispatching.

@CAD97
Copy link

CAD97 commented Jan 6, 2023

As @Mark-Simulacrum points out, not all traits can have a multi-type delegating implementation synthesized. The set of traits where this implementation can be synthesized is almost exactly one that already exists -- dyn-safe traits! Multi-type impl trait is type erasure fundamentally related to dyn Trait; anything that can be put into a vtable can just as easily be dispatched statically via enum.

I'd like to see the RFC place more emphasis on how this acts as a statically dispatched alternative to dyn Trait, rather than a more capable impl Trait. Because of the object safety concerns and type erasure involved, multi-type impl trait is imho much closer to dyn Trait than impl Trait, which is a singular opaque type and keeps all of the fun non-object safe functionality like receiverless associated functions, types, and constants.

There are of course still some differences; multi-type impl trait produces a Sized type compatible with where Self: Sized like those on Iterator returning Wrapper<Self>, and the enum_dispatch crate demonstrates how static enum dispatch is often significantly faster than dyn dispatch.

Being a "sized dyn Trait" isn't a fully correct specification either, though. A trait remains object-safe when defining associated functions without a receiver if that function is bound by Self: Sized, since the dyn Trait object is ?Sized and doesn't have that associated function. Like with dyn Trait, MVRPIT can't provide a definition of receiverless functions. Similarly, traits with associated types bound by Self: Sized remain object safe (but can't be used), and it's possible that associated const could be treated similarly in the future.

Making an object-safe trait no longer object-safe in an update is already currently a subtle pitfall that's easy to miss if a trait wasn't originally designed to be used as a trait object. Complicating the dyn safety rules to introduce MVRPIT seems unwise.

@clarfonthey
Copy link
Contributor

clarfonthey commented Jan 6, 2023

The RFC talks about not having a syntax for anonymous enums in general, but we have one: type alias impl trait (TAIT), i.e. type T = impl Trait. This can be thought of like a return-position impl trait that is captured and usable in other contexts.

Except, this opens up a load more questions. Likely, it would destroy inference and make it impossible to easily have a "defining usage" for TAIT, since conflicts could just be programming errors, or a genuine request for an enum type.

Honestly, this feels like a good indicator that this should at minimum be opt-in, but I'm not sure otherwise. But definitely, interoperability with TAIT is a must.

There's also all the work on dyn* Trait as well which might eliminate the need for this in a lot of cases.

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label Jan 6, 2023
@yoshuawuyts yoshuawuyts force-pushed the return-position-enum-impl-trait branch from 2ddaa30 to f64eadb Compare January 6, 2023 16:33
@Nemo157
Copy link
Member

Nemo157 commented Jan 6, 2023

One major discussion point from #2414 that seems missing here was around a desugaring to something that resembles InlineBox<dyn Trait, /* compiler generated max size of all possible options */>, explicitly using dynamic dispatch with the value stored inline rather than enum based dispatch. I seem to recall some benchmarking of that was performed, but after a quick skimming of the issue again I couldn't find it. (There was also significant discussion of what features would be required of a delegation feature to support this usecase).

Copy link

@CAD97 CAD97 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than wanting to see more details on the semantics of delegation, I only have two requests for the RFC:

  • Mention the alternative syntax options of enum impl Trait and enum dyn Trait1, along with motivation for why the chosen syntax is recommended.
    • There are limitations to the provider of multi-type RPIT not present in single-type RPIT; are those easily teachable?
    • Does changing between single-type and multi-type have any implications on what the caller can do?
  • Mention that coercion-based multi-type RPIT is essentially only available as the root return type, e.g. as -> impl Trait but not -> Wrapper<impl Trait>.
    • What are the implications on the previous point?
    • The RFC implies allowing explicit conversion-based usage of generic-wrapped multi-type RPIT, via -> Result<_, impl Error> and ? (which coverts using <(impl Error) as From>::from); what are the implications on type inference, and is this available to manual calls to From::from or is it impacting how ? is desugared?

My personal position is that we'll eventually get some form of this feature. Perhaps not this RFC — I expect delegation semantics will need to be ironed out separately first — and perhaps not soon, but eventually. The RFC is IMHO absolutely correct that this is a pain point worth addressing; it's just the details which need exploration.

Footnotes

  1. Disclaimer: my preferred syntax is enum dyn Trait, or some form of #[magic] dyn Trait. Modeling multi-type RPIT as actually using dyn Trait type erasure makes a lot of questions easier to answer, and potentially to teach. Using static enum dispatch instead of actual dynamic dispatch can be just an optimization, and treating it as semantically dyn type erasure even makes it easier to rationalize niching ((A | B) | C) together to just a single-layer (A | B | C) (and even (A | B) where A == C) since preserving separate type identity (e.g. via Any/TypeId of the opaque types) becomes possible to argue out.

    .... How does Any/TypeId interact with opaque single-type impl Trait + 'static, anyway? Can you dynamically extract the concrete type?

1. Find all return calls in the function
2. Define a new enum with a member for each of the function's return types
3. Implement the traits declared in the `-> impl Trait` bound for the new enum,
matching on `self` and delegating to the enum's members
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section absolutely needs to discuss what happens for non object-safe members, or at least mention that this is a concern discussed later.

Most of Iterator's methods aren't object-safe and can't be delegated. Iterator<Item=I> doesn't have any members without a receiver, but while some can still be delegated (e.g. fn last(self) -> Option<Self::Item>), those that return Wrapper<Self> can't be, and in fact can't even be implemented except by the default implementation of Wrapper::new(self).

To this point, it may be valuable to figure out delegation in a separate corequisite RFC, like how the generator/coroutine mechanism underneath async got its own separate eRFC despite carrying no intent to stabilize. The semantic questions around delegation are involved and interesting enough and mostly distinct from the questions this RFC is currently answering, being the interface design of automatically synthesized static dyn trait dispatch (which is itself involved enough to "deserve" its own RFC).

Tangentially related, on the point of dyn/delegation safety

It's been previously discussed the idea of weakening dyn safety of traits from an error to a warning, moving dyn safety to be a question on individual trait items. dyn Trait (or enum impl Trait) would then always be allowed, but just subset the trait items to those which are dispatch safe.

This is not without issue: for such "partially dyn safe" traits, dyn Trait: Trait no longer holds, so this can't magically make adding a first dispatch-unsafe item nonbreaking. It does make it less breaking, and perhaps offers a way in which making dyn trait an opt in (in a future edition) to object safety not as big of a "every trait should do this" deal, since &dyn Trait is still usable; declaring a dyn trait just means that dyn Trait: Trait (guarantees Trait is object-safe by today's definition).

On the other hand, the keyword generic initiative (~const) is leaning in the other direction: if you want to subset a trait like this, actually split the trait up into two parts, e.g. trait Static: Base, so you can write T: Static + ~const Base. The reasoning being that if there's a split between the "keyword safe" subset of the trait and "keyword unsafe," this can and should be aparent as part of the trait definition. This reasoning ports fairly cleanly to dyn and object safety. This missing feature is some way for Base to be "part of" Static rather than separate, making impl Static roughly impl Static + Base to differentiate it from impl Static + ~const Base, and also to make splitting like this possible semver-compatibly.

Much detail elided here, of course. Specialization (e.g. default impl Base for (impl Static)) and all of its problems are of course related though disjoint; it's closer to what I've called seen variously called "partial impls", "replaceable impls", or even "min min specialization" and "specialization without specialization" (roughly, default method bodies with additional bounds but no actual specialization).

}
}

// ..repeat for the remaining 74 `Iterator` trait methods
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This request is impossible, as implementing e.g. step_by via delegation is incorrect. The delegated implementation implied here is

fn step_by(self, step: usize) -> StepBy<Enum<'a>> {
    match self {
        Enum::A(iter) => iter.step_by(step), // : StepBy<Range<i32>>
        Enum::B(iter) => iter.step_by(step), // : StepBy<vec::IntoIter<'a>>
    }
}

This is obviously a type mismatch. The human-obvious desired impl is to not delegate step_by and inherit the default implementation, StepBy::new(self, step). The interesting thing here is that sometimes default-implemented trait methods are more playing the role of an extension trait; there exists no impl Iterator which doesn't use the default step_by implementation, because doing so is impossible (outside of core) due to the privacy of StepBy::new.

If the intent is that this delegating implementation is used, note that this is impossible; the coercion Wrapper<T> to Wrapper<(T | U)> is impossible in general because of the fundamental limitation that coercion cannot change layout behind indirection. The only layout-impacting coercion allowed is when unsizing e.g. [T; N] to [T], pointers change from thin to fat, and for the coercion to be allowed, there must only be a singular structural naming of the unsized type and it must be behind a singular indirection.

The rules about when coercion is allowed is complicated. We could in theory add a CoerceSized trait alongside CoerceUnsized and Unsize to encode places where layout-impacting coercion is allowed independently from unsizing (layout-preserving) coercion.. but coercion is already a complicated mess that we shouldn't make more complicated without reason.

To be fair, there do exist by-value coercions not covered by CoerceUnsized — e.g. for<T> ! -> T and {zst closure} -> fn() etc — but the former is arguably layout-compatible since ! doesn't exist, and the latter is IIUC the only case of a layout-changing coercion. (Even literals' inference type coercion is layout-compatible; the inference type has undecided layout.)

Comment on lines +320 to +461
One solution to make it compile would be to first map it to a type which can
hold *either* `i32` or `String`. The obvious answer would be to use an enum for
this:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This glosses over the complexity of choosing between (impl Iterator<Item=i32>) | (impl Iterator<Item=String>) and impl Iterator<Item=(i32 | String)>.

As part of the future sight section, going further into the difference and/or how to communicate the difference to the compiler isn't necessary, but the fact that the choice exists should be mentioned.

Multi-type RPIT gets to ignore this by only caring about root level alts.

@comex
Copy link

comex commented Jan 6, 2023

The set of traits where this implementation can be synthesized is almost exactly one that already exists -- dyn-safe traits!

One major difference is fns with type parameters, which always block dyn safety, but pose no major obstacle to synthesizability except perhaps in some cases where the Self type gets involved.

@yoshuawuyts yoshuawuyts force-pushed the return-position-enum-impl-trait branch from f64eadb to 7eafd50 Compare January 7, 2023 01:24
@yoshuawuyts yoshuawuyts changed the title RFC: Multi-Type Return Position Impl Trait (MVRPIT) RFC: Multi-Type Return Position Impl Trait (MTRPIT) Jan 7, 2023
@rodrimati1992
Copy link

rodrimati1992 commented Jan 8, 2023

How does this RFC interact with unsafe traits?

For example: bytemuck::Pod requires that the type can hold any bit pattern, which an enum wouldn't normally satisfy.

Pod can be used with return type impl Trait:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c555f9f0b2aa95761f2b3372ed317fe8

@programmerjake
Copy link
Member

another thing to consider:
what happens when a trait has a blanket implementation such that generating a delegating implementation of the trait for the generated enum would conflict with the blanket implementation?

pub trait Blanket {
    fn f(&self) {
        println!("called on {}", type_name::<Self>());
    }
}

pub trait Tr: Blanket {
    fn g(&self) {}
}

impl<T: Tr> Blanket for T {}

pub fn f(a: impl Tr, b: impl Tr, c: bool) -> impl Tr {
    if c { a } else { b }
}

impl Tr for () {}
impl Tr for &'_ () {}

pub fn g(c: bool) {
    // what does this print?
    f((), &(), c).f();
}

@burdges
Copy link

burdges commented Jan 8, 2023

Any idea what enum_dispatch lacks or requires rustc help? Are they handling where Self: Sized bounds nicely? A reasonable approach here would be:

fn multi_iter(x: i32) -> #[enum_dispatch] impl Iterator<Item = i32>

@CAD97
Copy link

CAD97 commented Jan 9, 2023

The main limitation of enum_dispatch is that it requires annotating the trait definition site as well as the usage, so that it knows what the trait definition is. There's nothing preventing combining enum_dispatch with the auto-enums approach, so long as enum_dispatch supports dispatching to generic types. If it doesn't, adding in some TAIT can be used to give names to the inferred types.

But also, as a fundamental limitation of procedural macros, enum_dispatch does not have type information. It quite literally cheats in order to know both the trait definition and the delegation set at the same time. Because of this cheating, it's incapable of handling multiple traits with the same name, and trying to use enum_dispatch for a trait defined upstream may or may not work, depending on how the compiler happens to use the proc macro server and whether this is a clean compile or not. I much prefer [ambassador]'s approach to hooking up delegation, if you couldn't tell.

Because type information is unknown, any procedural macro implementation (whether auto-enums, enum_dispatch, ambassador, or some other crate) requires either developer assistance to identify the set of types to dispatch to or to conservatively generate a separate enum case for every possible return path in a function. The compiler has type information, and can consolidate multiple instances of the same type into a single enum arm, and potentially even when layering MTRPIT, depending on what the runtime type downcasting semantics are.


ambassador should be added to prior art as well. It just does the delegation part, but it does so for arbitrary traits (which you must annotate directly or via facsimile), unlike auto-enums.

Comment on lines +402 to +405
TODO (tldr: because we're generating an anonymous enum, you can't downcast to a
concrete type. With single-value RPIT you have a concrete type which you _can_
downcast to. But with MTRPIT we're generating a type, meaning you can't downcast
into it)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like something the compiler could do? It knows about what types are in the enum so theoretically it should be able to check if any type in the enum is the downcast target and delegate the downcast?

Should this be under "Future Possibilities"?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any::downcast and friends downcast to whatever Any::type_id says. Any is blanket implemented for all Sized + 'static types; thus without specialization a dispatch enum will downcast to the dispatch enum, and downcast specialization would be extremely unsound (as Any downcast is a reinterpret cast including for owned memory).

Returning a trait object dyn Any + 'static will downcast to the individual hidden types, because the type id used is the one from the vtable of the hidden type.

Other traits in the ecosystem exist that use downcast(&self) methods or the provider api; those will of course dispatch properly.

Copy link
Contributor

@Fishrock123 Fishrock123 Jan 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced that is an impossible issue.

Keep in mind that the desugaring here is shown for simplicity, not for optimization.

Whatever vtable-like thing is used for the dispatch (be that an enum or on-stack vtable) could hold the typeid.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By reference, it could work, so long as the hidden type is at offset 0.

By stack value, it could maybe work, as you would need to use transmute_prefix instead of transmute because of the differing size.

The problem is the differing size once it's heap allocated behind a box. Box<dyn Any + 'static>::downcast<T> gives you Box<T> via a reinterpret cast. This is guaranteed; the way to write downcast_unchecked is Box::from_raw(Box::into_raw(it) as *mut T). A box with a different layout than the allocation is UB, realized when deallocating with a different layout than used for allocating.

It can work if you have MagicInline<dyn Any> which dereferences to the dyn Any of the hidden type, but that's because of the same container caveat that exists for every other container, that .type_id() will give you the type id of the container, not of the contained type. The container is also impl Any separately from the contained.

@CAD97
Copy link

CAD97 commented Jan 10, 2023

Downcasting brings up an important point: how do we handle stability?

For Iterator, implementing unstable-to-implement methods can impact performance of stable-to-call methods (e.g. try_fold is unstable to implement because of using Try), especially for std types which can just implement the root method everything else is implemented in terms of.

For Error, on the other hand, Error::type_id is unstable because it is unsound to implement except with the default body. This case is I believe saved by the proposed rule of only delegating a defaulted method if any of the delegatees have an implementation for it, but I don't recall if any other std traits are relying on any such tricks.

The safe answer is probably to say that items only get delegated if the delegating implementation can actually be written in the crate doing the delegation. Actually defining the preconditions, taking into mind even such things as unnameable types, which some crates do rely on1 to make things unimplementable outside of their crate, seems extremely difficult.

I also don't recall any mention of unsafe traits. The compiler cannot guarantee a delegating implementation is sound; thus delegation of an unsafe trait MUST NOT be done automatically without the unsafe trait opting in.

Footnotes

  1. Thankfully, I only know of cases which use unnamable types to make semver-exempt public-for-impl-reasons APIs more difficult to use. I can't shake the feeling I've seen a case of relying on an unnamable type being unnamable for soundness, though...

@CAD97
Copy link

CAD97 commented Jan 10, 2023

Some more edge cases I haven't seen mentioned yet:

  • If the trait puts any interesting bounds on Self (e.g. supertrait bounds), those also have to be delegated.
  • To that point, sealed traits. Commonly implemented by an unnamable supertrait, but also via required functions which name unnamable types. A simple example of a trait where individual types can be returned as impl Trait but can't be combined into an enum implementing the trait.
  • If the trait has associated types, it's somewhat unclear if iteratively generating more anonymous dispatch enums is even guaranteed to finish in a finite number of steps. It certainly means that
  • Returning Wrapper<Self::AssociatedType> can't be delegated if the associated type isn't constrained to be a singular type.

The more I consider it, the more I end up convinced that multi-type impl Trait has severe fundamental limitations not present for single-type impl Trait that just can't be papered over. None of them are immediately obvious when considering the idea; dyn Trait works, after all. But nearly every other corner I examine, there's a corner case which presents issues for synthesizing delegation implementations.

A fundamental motivation of the RFC as written is that Box<dyn Trait> does not necessarily itself implement Trait, it merely provides access to the type-erased object which implements Trait. But that very limitation can and is relied upon; that a crate knows the complete set of types that implements its trait. You can argue that traits where a new implementation showing up would be unsound should be unsafe, but even std1 defines safe traits where a downstream impl is unsound.

Either this feature will have the same interface semantics of dyn* Trait or it will need to define a new set of constraints for what traits are mtrpit-safe. This should probably be defined positively, as a list of things mtrpit-safe traits are allowed to do, rather than rule out any problematic cases. It's as of yet unclear whether this will result in a different API for the caller than impl Trait, assuming Trait is mtrpit-safe. But it will necessarily restrict what traits can be used, so it's never going to be as simple as "now you can use multiple types in RPIT."

Footnotes

  1. In std's defense, a) essentially all of the examples are from early Rust before unsafe trait with fully safe API was properly understood, b) most examples gate the ability to write unsound impls behind unstability, essentially pretending the unsound parts don't exist, and c) std is uniquely allowed to rely on compiler/language implementation details in ways user code isn't supposed to.

@yoshuawuyts
Copy link
Member Author

yoshuawuyts commented Jan 10, 2023

The more I consider it, the more I end up convinced that multi-type impl Trait has severe fundamental limitations not present for single-type impl Trait that just can't be papered over. None of them are immediately obvious when considering the idea; dyn Trait works, after all. But nearly every other corner I examine, there's a corner case which presents issues for synthesizing delegation implementations.

Yeah, I completely agree. I do believe the limitations are coherent enough that we can create a framework to handle them. But the starting point of this RFC was from a place where I (incorrectly) assumed these limitations might be at most minor; enough so that -> impl Trait returning more than one type would work almost identical to the way it does today.

That's clearly proving not to be true. unsafe Trait, fn () -> Self (Default), supertraits, associated constants, and delegation itself require some careful design and consideration. Nothing that I think we can't overcome. But I am unsure whether the scope of this RFC is now big enough that it may not align with the lang team's priorities. And I'm also unsure whether I can commit the time required at this point to see this through to completion.

@burdges
Copy link

burdges commented Jan 10, 2023

I think #2884 sounds more useful and largely subsumes this. If enums provide some speedup over indirection then people could use enum_dispatch more or similar.

@joshtriplett
Copy link
Member

@yoshuawuyts With my lang hat on: I don't object to an approach like this, if the corner cases were addressed (through support or through documentation of non-support). But I would definitely want it to use a different syntax than -> impl Trait, to flag the use of static multi-type dispatch like this. (I can also imagine giving the compiler the freedom to decide based on various factors whether to use an enum or some manner of dyn*.)

I don't know offhand whether this or some other approach would be best. @nikomatsakis, thoughts here, with respect to dyn* or otherwise?

@burdges
Copy link

burdges commented Jan 12, 2023

Just curious @CAD97 but do you know why "static enum dispatch is often significantly faster than dyn dispatch"? It's the compiler being able to transform the merged fns? Or CPU cache?

@CAD97
Copy link

CAD97 commented Jan 12, 2023

@burdges simply because the optimizer is typically better able to optimize a call to a known function than a call to an unknown function. If doing static dispatch to #[inline(never)] functions, the difference should be essentially zero. Compilers are also getting better at "devirtualization," slowly bringing the gap closer and down to perhaps just down to the extra Box allocation/indirection that often comes with dynamic dispatch.

@CalinZBaenen
Copy link

Honestly, this feels like a good indicator that this should at minimum be opt-in, but I'm not sure otherwise. But definitely, interoperability with TAIT is a must.

@clarfonthey, this has been discussed in the form of enum impl Trait syntax. I think this is a reasonable compromise, and since it's opt-in, while it's still under development, it could be more or less tweaked to squeeze around some of the issues mentioned.

@burdges
Copy link

burdges commented Feb 4, 2023

A delegation solution would broaden the #[derive(Trait)] toolbox in various ways, which sounds much more powerful, but also much more complex.

It's fairly clear enum Trait winds up morally more a flavor of dyn Trait than a flavor of impl Trait. As noted above, enum Trait: Trait is false, just like dyn Trait: Trait is false. It follows -> enum impl Trait should not be the syntax here. And the RFC's proposed -> impl Trait is simply impossible.

dyn Trait is a type because it's always instantiated the same. enum Trait is not always instantiated the same. enum Trait could seemingly be a trait however, roughly like FnOnce, FnMut, and Fn, so then -> impl enum Trait makes sense.

Amusingly dyn Trait: enum Trait so the trait enum Trait maybe useful more broadly, like in delegation. I think fn() -> Box<impl enum Trait> could return Box<dyn Trait> under the hood even.

We produce dyn Traits via casts as Pointer<dyn Trait> because this only requite changing pointer metadata. We do not want this for enum Trait so yeah one likely wants some keyword the constructs the concrete type of the enum. It's unclear if this should happen at the merger site or if maybe the compiler should infer all the variants more cleverly, ala

let mut it = enum it;
if backward { it = enum if.rev(); }
// loop { ... it = it.map(|x| ...) ...}  // error, cannot instantiate enum variants 
All this said, we'd maybe benefit more from `dyn Trait` being optimized better under the hood, but I'm unsure if I understood this whole discussion up thread.

Around this, where should the variant go? An impl enum Trait places the variant into the type itself, but enums cannot contain unsized type, making this less useful. If placement-by-return #2884 ever happens, then maybe we're better off trying harder to make dyn Trait do this, de facto making the metadata be the variant.

@shepmaster
Copy link
Member

what is delegation/proxying and how is it better than this solution?

@CalinZBaenen Wikipedia defines delegation and proxying as well as the related forwarding better than I will in a comment.

As I understand it, this RFC effectively proposes creating delegation as a compiler implementation detail and limited to enums. However, the concept of delegation can be useful for cases beyond that. As a few made up examples without much thought put to it...

  • I want to write an iterator adapter that counts how many times Iterator::next was called. A naïve implementation would forget to delegate Iterator::size_hint, degrading performance.
  • I want to combine a Foo (which implements FooTrait and a Bar (which implements BarTrait) into a Pair type and implement both FooTrait and BarTrait by calling the corresponding methods on Pair's member fields.

@CalinZBaenen
Copy link

Around this, where should the variant go?

@burdges, I don't quite understand the question.
I think we may be thinking about this feature with different mindsets. (F.ex. what is "the variant"?)

@burdges
Copy link

burdges commented Feb 6, 2023

Yes exactly, dyn Trait could simply be optimize for expected vtables, especially with #2884, so then zero new syntax, just -> dyn Trait runs faster sometimes.

@CalinZBaenen
Copy link

But wouldn't dyn Trait still be dynamic dispatch and not static-dispatch.
Or would one of the "optimizations" be to make it static? (At that point I wouldn't call it just an optimization.)

Yes exactly,

What are you agreeing to?

@CalinZBaenen
Copy link

It follows -> enum impl Trait should not be the syntax here.

Also, how did you come to this conclusion?

@CalinZBaenen
Copy link

@clarfonthey

There's also all the work on dyn* Trait as well which might eliminate the need for this in a lot of cases.

Not necessarily.
Apparently, according to this post, only types that are pointer sized or less can make use of dyn*, which could be "a lot" of cases, but it also equally leaves "a lot" of cases unaccounted for.
Also the size of a pointer, size_of::<usize>() varies depending on the architecture of your target, so that may make its usability unpredictable if you want to compile for all sorts of targets.
This would use an enum-like structure which would (seemingly) have no such limitation (except for ones that may already be imposed my the max size of an enum).

But definitely, interoperability with TAIT is a must.

I feel like this would have to inherently exist.
Sure, I don't believe a typealias can alias all types (unless it can), but I do believe if a typealias can alias a "normal" impl T it can also alias an {enum impl T}.

Except, this opens up a load more questions. Likely, it would destroy inference and make it impossible to easily have a "defining usage" for TAIT, since conflicts could just be programming errors, or a genuine request for an enum type.

Honestly, this feels like a good indicator that this should at minimum be opt-in,

I don't know about you, or anyone participating in this RFC (now or in the future), but I'm pretty set on enum impl Trait or impl enum Trait, it's concise, pretty much gets to the point, and is opt-in. Perfect.

@CalinZBaenen
Copy link

I'd like to present my avocation for this feature.
Whether dyn* Trait works out well or not, I believe this still deserves a spot in Rust.

This is my very detailed response about why I believe this feature is good.
Any questions may be asked in the comments there or here (in reply to me by @ing).

@programmerjake
Copy link
Member

@CalinZBaenen I'd like to point out that returning Self is perfectly fine and should be allowed for the cases where the method also has a parameter which can be used to deduce which type is meant.
so, impl enum Clone is perfectly valid since fn clone has a &self parameter so we know the Self type must match the passed-in Self.

pub trait ImplEnumIsValid: Clone {
    fn clone2(v: &Self) -> Self; // note parameters don't have to be `self` to work

    // impl enum would have to ensure the two Self's match
    fn foo(a: &Self, b: &mut Option<&Self>) -> Result<Self, ()>;

    // also works since `which` would become an enum with `PhantomData` fields,
    // the discriminant allows determining which `Self` type to use
    fn bar(which: PhantomData<Self>) -> Self;
}

@programmerjake
Copy link
Member

    fn bar(which: PhantomData<Self>) -> Self;

this case is an actually pretty interesting extension imho, which's type would have to become something like enum PhantomData<impl Trait>, so that implies enum impl Trait is a more correct way to spell it than impl enum Trait...

@CalinZBaenen
Copy link

@programmerjake

so that implies enum impl Trait is a more correct way to spell it than impl enum Trait...

I mean, we got the "roughdraft" name down, one of the two orderings of impl enum ... or enum impl ..., bikeshedding can be done more once we're closer to this RFC coming to fruition.

so, impl enum Clone is perfectly valid since fn clone has a &self parameter so we know the Self type must match the passed-in Self.

But how do we initially resolve the type for Self?
We'd need to essentially know the type the call will return before the function is called, which seems impossible. (Do you have any ideas around this?)

this case is an actually pretty interesting extension imho, which's type would have to become something like enum PhantomData<impl Trait>,

Howcome?

so that implies enum impl Trait is a more correct way to spell it than impl enum Trait...

How is that implied by the previous statement?

@CalinZBaenen
Copy link

Also, the DEV post has been updated.
I have now posted a comment with theoretical output code for one of the testcases I had provided, originating from this Discord message.

@programmerjake
Copy link
Member

so, impl enum Clone is perfectly valid since fn clone has a &self parameter so we know the Self type must match the passed-in Self.

But how do we initially resolve the type for Self? We'd need to essentially know the type the call will return before the function is called, which seems impossible. (Do you have any ideas around this?)

yes, simply use the exact same type as self:

fn f(v: bool) -> enum impl Clone {
    #[derive(Clone)]
    struct A(String);
    if v {
        A(String::from("blah"))
    } else {
        42i32
    }
}

becomes:

fn f(v: bool) -> impl Clone { // not always just `impl Trait`
    #[derive(Clone)]
    struct A(String);
    enum Ret {
        A(A),
        I32(i32),
    }
    impl Clone for Ret {
        fn clone(&self) -> Self {
            // here's how we determine which clone() to call:
            match self {
                Self::A(v) => Self::A(A::clone(v)),
                Self::I32(v) => Self::I32(i32::clone(v)),
            }
        }
    }
    if v {
        Ret::A(A(String::from("blah")))
    } else {
        Ret::I32(42i32)
    }
}

this case is an actually pretty interesting extension imho, which's type would have to become something like enum PhantomData<impl Trait>,

How come?

Because the value doesn't actually contain any impl Trait values, instead the type after enum is just an arbitrary fully-general type with an impl Trait inserted in it somewhere. Nevertheless, the enum dispatch still works fine:

pub trait MyTrait {
    fn bar(which: PhantomData<Self>) -> Self;
}

pub fn foo<T: MyTrait>(use_t: bool) -> enum PhantomData<impl MyTrait> {
    struct Fallback;
    impl MyTrait for Fallback {
        fn bar(which: PhantomData<Self>) -> Self {
            println!("Fallback::bar");
            Fallback
        }
    }
    if use_t {
        PhantomData::<T>
    } else {
        PhantomData::<Fallback>
    }
}

becomes:

pub trait MyTrait {
    fn bar(which: PhantomData<Self>) -> Self;
}

struct Fallback;

impl MyTrait for Fallback {
    fn bar(which: PhantomData<Self>) -> Self {
        println!("Fallback::bar");
        Fallback
    }
}

enum EnumImplMyTrait<T> {
    T(T),
    Fallback(Fallback),
}

enum EnumPhantomDataImplMyTrait<T> {
    T(PhantomData<T>),
    Fallback(PhantomData<Fallback>),
}

impl<T: MyTrait> EnumImplMyTrait<T> {
    fn bar(which: EnumPhantomDataImplMyTrait<T>) -> EnumImplMyTrait<T> {
        match which {
            EnumPhantomDataImplMyTrait::T(v) => EnumImplMyTrait::T(T::bar(v)),
            EnumPhantomDataImplMyTrait::Fallback(v) => EnumImplMyTrait::Fallback(Fallback::bar(v)),
        }
    }
}

pub fn foo<T: MyTrait>(use_t: bool) -> EnumPhantomDataImplMyTrait<T> {
    if use_t {
        EnumPhantomDataImplMyTrait::T(PhantomData::<T>)
    } else {
        EnumPhantomDataImplMyTrait::Fallback(PhantomData::<Fallback>)
    }
}

so that implies enum impl Trait is a more correct way to spell it than impl enum Trait...

How is that implied by the previous statement?

because enum impl Trait is the simplest case of enum SomeTypeWith<impl Trait>::InIt where that's enum $ty where $ty is a type that must have impl Trait somewhere in it (only once?).

@CalinZBaenen
Copy link

Thank you for your response, @programmerjake. I understand much more clearly.
Also, nice work making manuscripts of what the converted code would look like. (Hopefully that will make this feature and its intent more clear to future readers.)

@burdges
Copy link

burdges commented Feb 7, 2023

I'd like to point out that returning Self is perfectly fine and should be allowed for the cases where the method also has a parameter which can be used to deduce which type is meant.

I'd think no actually..

enum Trait needs concrete positive rules, like dyn Trait does, meaning support for niche cases ends somewhere. As an analogy to ImplEnumIsValid, it's clear how DynCouldBeValid could compile, but doing so oversteps the language's complexity budget.

trait DynCouldBeValid {
    fn dup(&self) -> Box<Self>;
}

fn bar(foo: &dyn DynCouldBeValid) -> Box<dyn DynCouldBeValid> {
    foo.dup()
}

I'd think enum Trait needs exactly the same positive rules as dyn Trait if only for the complexity budget.

In fact, your example convinces me enum Trait would be harmful feature. At least for dyn Trait you could always add object safe methods, and where Self: Sized clauses, especially with #2884, as well as inherent methods on dyn Trait itself, but enum Trait manages this less well.

Instead, dyn Trait should be optimize for expected vtables, perhaps via #[hot] annotations, maybe added by profiler, or whatever. A profiler guided choice between dyn Trait and static enum dispatch would be much more broadly useful.

@CalinZBaenen
Copy link

@burdges

Instead, dyn Trait should be optimize for expected vtables, perhaps via #[hot] annotations, maybe added by profiler, or whatever.

This could be a nice thing to do as well, but I feel like static-dispatch will always have some usacase(s) over dynamic-dispatch.
Not to mention you can hold enum Trait [<-- Not a concrete type.] values directly in a variable whereas you can't with dyn Trait values.

In fact, your example convinces me enum Trait would be harmful feature.

Just because of their use alone or in general? [I feel like it would be unfair to penalize for their usage.]
What do you find harmful about it?

I'd think enum Trait needs exactly the same positive rules as dyn Trait

  1. "positive rules"?
  2. Not necessarily. I feel like they are able to have slightly different semantics given the situations they apply to.

I'd think no actually..

So maybe I was right all along.

@yoshuawuyts
Copy link
Member Author

As I indicated here, I currently don't have the bandwidth to continue working on this. This RFC needs a fair bit of rewriting to capture the points raised in discussion, and even then still has a number of open questions. I'm going to close this issue to reflect my inability to commit to shepherding this RFC.

Thanks for the input and discussion everyone; I hope that this RFC and discussion will serve as a useful reference for future work!

@yoshuawuyts yoshuawuyts closed this Feb 7, 2023
@CalinZBaenen
Copy link

@burdges, for the returning Self thing, I had an idea. What if for enum impl Trait it just returned another enum impl Trait (and did whatever with the self receiver, if any), that way you couldn't get into any weird state.
Yes, I know this would kind of conflict with the whole definition of Self, but it gets us around one issue and is perfectly adequate considering we're still following whatever constraints are yet to be charted.

(And they will be charted, because as an advocate for this feature, I'm going to vastly expand on this RFC.)

@CalinZBaenen
Copy link

@charlie-lobo

Personally I agree with the need for explicitness, but disagree with the semantic implication of the position. If the user is getting a hidden enum, or a dyn type, or all those details should be hidden, it's an implementation detail. Instead I'd specify it inside the function, so something like return enum data.

But if we place enum with the return, doesn't this mean we can't use a theoretical enum impl Trait type in other places where we could usually use impl Trait?
If we do it the other way though we basically enable it in all cases.

@charlie-lobo
Copy link

@CalinZBaenen

But if we place enum with the return, doesn't this mean we can't use a theoretical enum impl Trait type in other places where we could usually use impl Trait?

Well a door closed sometimes means a window opened. We could extend this to be used on any expression. Basically enum impl Trait rather than specifying we are returning an anonymous impl type that uses an enum, means the expression is supposed to be put into an anonymous enum type, of which we know that it impls all traits by delegating to its passes. So we could do something like

// Alternatively we could specify it on the other side
// when we want to specify it elsewhere
// let secret_var = enum impl Trait match foo {
let secret_var: impl Trait = enum match foo {
    bar => SomeTypeImplementingTrait{x:1, y:"hello"},
    baz => AnotherTypeThatAlsoIsTrait(3.14),
};

So what I was proposing is rather than have an "abstract type" that coerces into an enum that delegates trait implementation, have an "expression modifier" that says that the expression that follow should have all its elements put into an enum where the enum implements (by delegation) a specific set of traits. Because the type is anonymous, a voldermort type, you can only store it by typing it as impl Trait or some of the other types such as std::any, but I am assuming that we do lose the possible types.

As to why I think it's better that way. I am thinking of code evolution.

So say I have a function fn foo(elem: Id) -> impl Trait. This function can sometimes return a u32 or a i32, depending on context, so the types are pretty small. OTOH Trait is a non-trivially sized trait, with a notable amount of methods, but is otherwise pretty simple. So I could return a dyn Trait without having to change the type of the function. But this brings in the VTable and that's expensive. I'd rather return an enum. I could make my own enum, but why not simply return a valid enum instead?

But later we get a new option, that is actually [u64; 8] suddenly things get pretty heavy to return on the enum (which has to be that big). Suddenly the dyn Trait option doesn't sound that bad. So I'd want to change the actual return type to be a dyn Trait rather than enum impl Trait, but the function signature doesn't change. After all this was only an implementation detail from the view of any user, so calling this a "breaking change" would be non-ideal. Unless we are going to talk about ABI stability and making that a breaking change...

Now that means that the feature isn't that useful as a type-signature, IMHO. Because once I choose it, I am stuck with a certain data-type profile, which kind of misses the point of this abstract features. It'd be better to explicitly add an enum that handles that, so it's easy to understand the size and effect of it on other things and realize when adding another option is going to have serious implications, because once I comitted to the enum, I am stuck with the enum, the boilerplate isn't that bad honestly (a macro to make the delegating impl would suffice). But if it's simply another tool I can swap out, it becomes much more attractive as a solution that is a good place to start, and later we can move out of. And we still keep the ability to use it anywhere we use impl Trait, though it can also be used with generics.

@CalinZBaenen
Copy link

@programmerjake

another thing to consider:
what happens when a trait has a blanket implementation such that generating a delegating implementation of the trait for the generated enum would conflict with the blanket implementation?

As it turns out, this is not a special case.
This would (probably) work just how it works normally, by only calling ()'s implementation. Here's an example on the Rust Playground.
... And using heterogeneous types results in the correct output.

@CalinZBaenen
Copy link

CalinZBaenen commented Feb 8, 2023

@charlie-lobo

So say I have a function fn foo(elem: Id) -> impl Trait. This function can sometimes return a u32 or a i32, depending on context, so the types are pretty small. OTOH Trait is a non-trivially sized trait, with a notable amount of methods, but is otherwise pretty simple. So I could return a dyn Trait without having to change the type of the function. But this brings in the VTable and that's expensive. I'd rather return an enum. I could make my own enum, but why not simply return a valid enum instead?

  1. By impl Trait do you mean MTRPIT (enum impl Trait)?
  2. If the answer to number one is yes, like I assume it is, how can you return a dyn Trait? When and why does dyn get involved?
  3. When you ask "but why not simply return a valid enum instead", what do you mean by "valid"? What makes your own enum "invalid"?

As to why I think it's better that way. I am thinking of code evolution.

How does this help code evaluation?
Could you provide a direct comparison between the enum impl Trait syntax and the impl Trait and match {expression}? Preferably with comments that may explain anything that may require a question.

Well a door closed sometimes means a window opened. We could extend this to be used on any expression.

How will the compiler know if one impl Trait is of the normal or multitype variety?
That metadata would have to be packaged. (Though I guess enum impl Trait requires metadata anyways.)

@charlie-lobo
Copy link

@CalinZBaenen

By impl Trait do you mean MTRPIT (enum impl Trait)?

Kind of. What I mean is that impl Trait remains as the return type. The thing that changes to allow MTRPIT, AFAICT is the function itself, to the caller it didn't have to be different from any impl type, things are evolving a bit more. Just as you can return a dyn Trait from a function that does this. The wrapping of the multiple types into the shared interface happens inside the function body either way.

This is why the original statement didn't have any explicit statement. I agreed with the need for an explicit statement, but it would be for the benefit of someone looking at the function itself, not someone simply calling it. I'm just moving it around.

If the answer to number one is yes, like I assume it is, how can you return a dyn Trait? When and why does dyn get involved?

Actually that's a very good point, I should clarify. I am not thinking of what dyn currently is, but instead of what dyn* would allow us to be.

Now in a world where dyn* Trait and impl Trait why would we ever want to write: fn foo() -> dyn* Trait when we could write it as fn foo() -> impl Trait and then choose whatever type it is? Well the answer is simple: it's because dyn* Trait is an actual concrete type, even though it's hidden the details of the actual type, it does so behind a well known interface (a virtual table), we have a pointer to a piece of memory whose size is unknown, but this is very workable. If I have two instances of dyn* Trait I can swap them, or mess around with them, because I know the details concretely. That is dyn Trait is not an anonymous type, it's a concrete type that lets us use multiple types as one at runtime, size was an issue, but again dyn* Trait is all about solving that.

So dyn* Trait gives us something that impl Trait doesn't. But there's many traits we can't use dyn on, but we can use impl on. So it makes sense to have both. Ok so this is easy.

What I am arguing is that impl enum Trait is not like that. The thing is that if I have two impl Trait they may be radically different types behind the scenes, I cannot treat them "as the same thing" (put them in an array or such). The same thing here with impl enum Trait, since two different functions may have two different enums created, they cannot be considered interchangeable as dyn* Trait would be. That is impl enum Trait is anonymous at static time just like impl Trait and therefore has the same limitation. So there's no benefit in exposing this as a return type over just returning an impl Trait, there's no difference from the point of view of a user of the type. Only when we create the instance do we care.

So while it makes sense to want to specify a function that returns dyn* Trait instead of a impl Trait, there's no point, IMHO, to have a function that returns impl enum Trait rather than impl Trait.

When you ask "but why not simply return a valid enum instead", what do you mean by "valid"? What makes your own enum "invalid"?

That was a poor choice of words. Why not return an anonymous enum instead? If no-one, including myself, will be able to recover that this is an enum, and deconstruct it, why even go through the hassle of doing it by hand? Let the compiler build it for me.

How does this help code evaluation?

Not evaluation evolution. Every function has an publicly visible part that shouldn't change (except to become a super-set of what it was) and a private part that can change easily. The public-part is more than the type, it's if you're using generic interfaces or impl, it's the kindness, etc. etc. And annotations can be part of this, when you expose them it becomes a problem, as people may want to switch.

Could you provide a direct comparison between the enum impl Trait syntax and the impl Trait and match {expression}? Preferably with comments that may explain anything that may require a question.

Not sure what you mean here?

I guess that comparing the example of people, if we had something that looks like

fn foo(b: bool) -> impl enum Trait {
     if b {
         return Foo{};
     } else {
        return Bar{};
}

I would rather it be written as

fn foo(b: bool) -> impl Trait {
    if b {
        return enum Foo{};
    } else {
        return enum Trait Bar{};
    }
}

Second return has excessive large. Or alternatively for this specific case:

fn foo(b: bool) -> impl Trait {
   enum if b {
        Foo{}
    } else {
        Bar{}
    }
}

As enum here is something that makes the expression a result of all its possible expressions. Type inference tells us it should be impl Trait but you could specify a more abstract thing.

How will the compiler know if one impl Trait is of the normal or multitype variety?

This is just sugar code for making an enum, implementing the trait, and returning it. It only matters in the function body and is an implementation detail. The compiler only needs to know this when it's creating the body. When calling all the compiler needs to know is that the returned type implements Trait. Now how does the compiler end up finding what is the actual value? The same way it does for every other function that returns impl Trait: the compiler always knows what is the true type, it just doesn't share that with the user on purpose.

That metadata would have to be packaged. (Though I guess enum impl Trait requires metadata anyways.)

Yup, you're right it does need to happen. Both cases end up in the same machine code, with the same tag bits added to allow us to pick on runtime which function to call. In both cases we do have a well sized, fully-stack bound piece of data.

The difference is not in the end-result program. The difference is in how the compiler works with it, and how the programmers that meet this will have to work around it or with it.

@CalinZBaenen
Copy link

Not evaluation evolution.

Leaving this reply in advance; my bad.
I must have been tired or something.

@CalinZBaenen
Copy link

CalinZBaenen commented Feb 9, 2023

It only matters in the function body and is an implementation detail. The compiler only needs to know this when it's creating the body.

This is a good argument.
I will nitpick that most people don't look at the function bodies in other libraries.
They usually just check the signature and read the according documentation

I understand your suggestion and I can, on some level, come to agree.
The main reason I don't like it is because you end up writing "enum" a lot more than I think you should have to.
I do see the utility in enum if and friends, but there are places where you might not be able to use one catch-all enum, which could lead to an overwhelming amount of enum <statement>s.
I do think your third example is a flattering one, it definitely reduces on the excessive enum-spam, I just don't currently know what to think about enum <statement> syntax. (enum would have to be appliable to anything that can return a value, including loop.)

@CalinZBaenen
Copy link

CalinZBaenen commented Feb 9, 2023

[Misc. replies.]

The thing that changes to allow MTRPIT, AFAICT is the function itself, to the caller it didn't have to be different from any impl type,

AFAICT?

Actually that's a very good point, I should clarify. I am not thinking of what dyn currently is, but instead of what dyn* would allow us to be.

Well, with the usize sizelimit, but yeah.

I guess that comparing the example of people, if we had something that looks like

What's with the Go casing convention?
It looks jarring when all functions are snake_case. (I literally just got used to it.)

@CalinZBaenen
Copy link

Oops, I forgot to "mention" you, @charlie-lobo.
(I don't know if editing messages will alert you or not.)

@CalinZBaenen
Copy link

One more thing I thought of.
What if not all codepaths have an enum? Is that return value still allowed and implicitly made one of the enum-dispatch values because it's -> impl Trait or will it be an error since other items have enum?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.