-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
RFC: Multi-Type Return Position Impl Trait (MTRPIT) #3367
Conversation
I would prefer an explicit syntax, such as |
a5447ff
to
2ddaa30
Compare
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
|
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 I would strongly advocate for an |
|
||
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 trait
s 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.
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 I'd like to see the RFC place more emphasis on how this acts as a statically dispatched alternative to There are of course still some differences; multi-type impl trait produces a Being a "sized 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. |
The RFC talks about not having a syntax for anonymous enums in general, but we have one: type alias impl trait (TAIT), i.e. 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 |
2ddaa30
to
f64eadb
Compare
One major discussion point from #2414 that seems missing here was around a desugaring to something that resembles |
There was a problem hiding this 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
andenum dyn Trait
1, 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 toFrom::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
-
Disclaimer: my preferred syntax is
enum dyn Trait
, or some form of#[magic] dyn Trait
. Modeling multi-type RPIT as actually usingdyn 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 semanticallydyn
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)
whereA == C
) since preserving separate type identity (e.g. viaAny
/TypeId
of the opaque types) becomes possible to argue out..... How does
Any
/TypeId
interact with opaque single-typeimpl 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.)
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: |
There was a problem hiding this comment.
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.
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 |
f64eadb
to
7eafd50
Compare
How does this RFC interact with unsafe traits? For example:
|
another thing to consider: 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();
} |
Any idea what
|
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 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. |
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) |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Downcasting brings up an important point: how do we handle stability? For For 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 Footnotes
|
Some more edge cases I haven't seen mentioned yet:
The more I consider it, the more I end up convinced that multi-type A fundamental motivation of the RFC as written is that Either this feature will have the same interface semantics of Footnotes
|
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 That's clearly proving not to be true. |
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. |
@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 I don't know offhand whether this or some other approach would be best. @nikomatsakis, thoughts here, with respect to |
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? |
@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 |
@clarfonthey, this has been discussed in the form of |
A delegation solution would broaden the It's fairly clear
Amusingly We produce
Around this, where should the variant go? An |
@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...
|
@burdges, I don't quite understand the question. |
Yes exactly, |
But wouldn't
What are you agreeing to? |
Also, how did you come to this conclusion? |
Not necessarily.
I feel like this would have to inherently exist.
I don't know about you, or anyone participating in this RFC (now or in the future), but I'm pretty set on |
@CalinZBaenen I'd like to point out that returning 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;
} |
this case is an actually pretty interesting extension imho, |
I mean, we got the "roughdraft" name down, one of the two orderings of
But how do we initially resolve the type for
Howcome?
How is that implied by the previous statement? |
Also, the DEV post has been updated. |
yes, simply use the exact same type as 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)
}
}
Because the value doesn't actually contain any 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>)
}
}
because |
Thank you for your response, @programmerjake. I understand much more clearly. |
I'd think no actually..
I'd think In fact, your example convinces me Instead, |
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.
Just because of their use alone or in general? [I feel like it would be unfair to penalize for their usage.]
|
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! |
@burdges, for the returning (And they will be charted, because as an advocate for this feature, I'm going to vastly expand on this RFC.) |
But if we place |
Well a door closed sometimes means a window opened. We could extend this to be used on any expression. Basically // 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 As to why I think it's better that way. I am thinking of code evolution. So say I have a function But later we get a new option, that is actually 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 |
As it turns out, this is not a special case. |
How does this help code evaluation?
How will the compiler know if one |
Kind of. What I mean is that 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.
Actually that's a very good point, I should clarify. I am not thinking of what Now in a world where So What I am arguing is that So while it makes sense to want to specify a function that returns
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.
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.
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
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
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. |
Leaving this reply in advance; my bad. |
This is a good argument. I understand your suggestion and I can, on some level, come to agree. |
[Misc. replies.]
AFAICT?
Well, with the
What's with the Go casing convention? |
Oops, I forgot to "mention" you, @charlie-lobo. |
One more thing I thought of. |
Summary
This RFC enables Return Position Impl Trait (RPIT) to work in functions which return more than one type:
Rendered