-
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 for anonymous variant types, a minimal ad-hoc sum type #2587
Conversation
We will go to space today!
UPD: See this Pre-RFC. To not undermine the work put into this RFC, but I think that the proposed solution is quite sub-optimal and we should pursue the proper "anonymous union-types" (i.e. for which One possibility for how this functionality could look is: struct Err1;
struct Err2(u32);
fn foo() -> (u32 | () | A | B | C) { .. }
fn bar() -> Result<(), (Err1 | Err2)> { .. }
match foo() {
// if result has type A, the value will be stored in the `a`
a: A => { .. }
// we can match with a value as well
1: u32 => { .. }
// we probably can allow omitting explicit type if it can be inferred
() => { .. }
// `b` will have type (A | B | C), probably we don't want to diverge too much
// from how matching works today
b => { .. }
}
match bar() {
Ok(()) => { .. },
Err(Err1) => { .. },
Err(e: Err2) => { .. },
} For generic matching problem I think we should just specify that match arms are tested in order, so if on monomorphization of matching on match uv_enum {
v: u32 => { .. },
v: u32 => { .. },
} In other words only the first match arm will be executed, and the second arm will be removed. (though compiler should probably emit Regarding memory representation of this type union __AnonUnionPayloadABC { f1: A, f2: B, f3: C }
struct __AnonUnionABC {
discriminant: TypeId,
payload: __AnonUnionPayloadABC,
} It will make converting from Regarding how types like |
|
||
// And then match on it | ||
match x { | ||
(_ | _)::0(val) => assert_eq!(val, 1_i32), |
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.
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.
Changed, especially since the angled brackets are also required for tuple associated items. I'm not particularly fond of the kirby-boss syntax, but consistency helps ease implementation, which is my primary concern.
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.
Changed back, it's now an unresolved question, with me leaning towards the syntax depicted above.
Assuming that we do want to provide structural coproducts... As I noted on the internals thread I think that the most natural way to provide structurally typed coproducts is to take type Foo = Bar | Bar(u8, f32) | Quux { field: String };
// This variant is inspired by or-patterns to give a sense of disjunction.
// Tho this syntax is likely ambiguous if we have `pat ::= ... | pat ":" type ;`
// so we will instead need:
type Foo = (Bar | Bar(u8, f32) | Quux { field: String }); and (2): type Foo = enum { Bar, Baz(u8, f32), Quux { field: String } }; Here (2) is the most minimally different syntax from nominally typed Another benefit of both (1) and (2) is that only the type grammar changes. Zero changes need to happen to the expression and pattern grammars; this is beneficial for making the language easier to learn. To pattern match and construct things, you simply write: let a_foo: Foo = Baz(1, 1.0);
match a_foo {
Bar => expr,
Baz(x, y) => expr,
Quux { field } => expr,
} Both of (1) and (2) also have nice properties:
fn foo() -> enum { A, B } {
...
} This is useful for the structural nature because it allows you to invent new "flag conditions" freely.
|
@newpavlov I think that should be a different proposal on its own. I also think that sum types are way less problematic than union types exactly because they don't try to second guess the user and filter out duplicates structurally. That filtering quickly becomes a highly nontrivial issue as soon as you start adding e.g. generics (which was pointed out on the internals forum as a possibility). |
That said, even for anonymous variants / anonymous sum types, I don't find the motivation convincing enough and the gains in convenience sufficient compared to the extent to which it grows the language. I have already explained why in the internals thread. However, it seems that for opinions to be counted in an RFC at all, they have to be re-iterated here, so there we go. |
A large factor in me deciding to use positions rather than identifiers to identify variants was that Rust currently lacks the ability to have placeholders for names and to generify over name, and to add that would require extra groundwork to make happen. And similar proposals which had just a few more conveniences than this proposal have been shot down for complexity! Here, if you don't believe me: Similar proposal, which is almost the same as this one besides being less detailed and using a more ergonomic syntax |
A quick read of the grammar indicates that parenteses are not necessary. Is this correct? Furthermore, we should be clear that a sum can contain unnameble summands, yes?
|
The parentheses are not necessary, but I put an eye on the possibility on later extensions, and if multi-field variants become a thing and commas are used to separate the fields of a variant, I don't want ambiguity to result from things like this: (f32 | i32, i32 | f64); // Tuple of two anonymous variant types, or an anonymous variant type whose second variant has multiple fields? And I'm pretty sure if you make the number of variants clear and the type of each variant unambiguous, it should work. Your example as is wouldn't, but this would, as it specifies that there are two variants in one of the match arms: fn foo() -> impl Copy {
if cond {
<(_|_)>::0(|| 0)
} else {
<_>::1(1)
}
} |
I don't think that the previous proposals were rejected on the grounds of complexity per se, but on the grounds that the complexity was too high with respect to the advantages offered. Personally, I don't think this proposal changes that in any significant way. (Certainly not if the intention is for follow-up proposals to put things into roughly the same state. If you can't afford the car, offering to purchase the parts and assembly separately does not make it cheaper.) |
That analogy kind of implies that the proposal will be useless unless all the doodads are in place, which is not the case here. I counter-propose the analogy of a car mortgage, where the payment might be somewhat more in the long run, but spreading out the costs makes it more affordable than paying it all at once. And this proposal is designed to be easily extendible by the ecosystem even in the short term, so the doodads can be hashed out by competeing ecosystem solutions rather than being stuck in RFC discussions for months at a time and risking nonacceptance. |
I think the problem with this feature, and a few others, including more union-y one, is that it tries to preserve pattern-matching as a primitive even for unknown types.
What I want to use an anonymous "choice of one type from several" for, is not pattern-matching, but static trait dispatch - which would be done automatically by the compiler, with an enum-like tag. That is, We can probably even make e.g. EDIT: to give a concrete example of the usecase I'm talking about: fn foo() -> impl Iterator<Item = X> {
if cond() {
bar()
} else {
baz()
}
} I want this to work without using |
Yes, I admit as much that this proposal doesn't reach into that area. It's similar to problems from not being able to unsize into a dynamic trait object an enum whose variants consist of a single field, all of which implement a particular trait, automatically. (At least not without the help of macros of some kind, the most recent reasonably popular one of which was implemented via
Perhaps this might be a feature that is worth the extra complexity to implement initially, but as of now, I'm not convinced that it won't sink the rest of the proposal under its own weight. |
Fair enough. I just don't see the point of a related RFC that doesn't tackle |
I donno if I understand @eddyb but yes traits sound key here: We could have |
FWIW, I never meant vtables, you'd still have tags but only where needed. |
I've changed the syntax of variants (for both calling and matching) from stuff like |
Ie. you want union types, not sum types. That is fine. They are problematic in a language with real, parametric generics though, for a number of reasons, and even in the absence of generics, they can carry surprises..
And then basically every |
Aaah, okay – sorry, misunderstood that. That, I would support. |
@H2CO3 I'm actually be curious of what you mean by interactions between union types and parametricity if you can't pattern-match on them (unless you have a proof of disjointness)? In fact, we can rely on lifetime parametricity to even do |
@eddyb I'm not sure what you mean about pattern matching. I've explained the problem in the post I linked. What should the following code do? fn foo<T>(x: T | bool) {}
foo::<bool>(false); I.e., the question is: if (I believe the codegen issue is much less serious if these union types are not exposed at the language level, because I could then imagine just "not doing anything", i.e. duplicating dispatch logic for every variant of the same type, which could take up more space but it would be otherwise 100% correct wrt. semantics, and probably let MIR optimization get rid of it. But if you mean that the language should actually expose these types in a manner that they can be spelled out, other than behind an |
@H2CO3 The conditions for
I believe that includes |
Certainly. I know that and you know that. But how does the union operation itself manifest at the language surface if you are not allowed to look at the type signature after instantiating As mentioned before, I'm not concerned about trait implementations alone. The low-level details can certainly be implemented in a number of ways, including the naive approach I described above. The problem is with situations which were, again, mentioned in the internals thread, for example if spelling out the union of a type with itself is to be disallowed. Enforcing that condition can only happen after generic instantiation. |
It seems like the quadratic text space required, while worse than what could be, shouldn't be a concern in practice. For small numbers of cases, typing the correct number of underscores and vertical bars shouldn't take up much time and space. For larger number of cases, one can synthesize a proc macro with relative ease that generates a anonymous variant type placeholder from the number of variants desired, which clamps down the text space usage of a full match to linearithmic. If that isn't enough to not flood the screen with type declaration macros, then I say you're getting to the point where you should probably rethink what in the world you're doing with so many variants, and maybe consider refactoring to a proper named enum, a trait, or a struct/tuple of orthogonal enumerable parts. |
Yeah. Another thing that occurred to me right after I posted my previous comment was that there already is a quadratic, or at least n*k blowup, which exerts downward pressure on the number of components in tuples as well as anonymous sums -- namely you have to write out all of the components each time in type signatures. Unlike with named types. Now, you could introduce a |
Opposed. High cost addition -- implementation and cognitive load -- minimal win over the sums we have. Also we already had anonymous sums early on and removed them. This is revisiting a reduction intentionally made in the past. |
Can you show me these early Rust anonymous sums? Nothing of this sort came up when I dug into previous proposals for anonymous sum types in Rust. |
@rfcbot fcp postpone I'm going to move to postpone this proposal. While I do think that there is potential utility to this feature, I also think that the time is not ripe. Although the roadmap is not set, I think it very likely that our focus is going to be on "closing out" many of the language additions that are already in flight (e.g., specialization and so forth) and not on adding a whole new base form of type. Moreover, we already have troubles with the lack of variadic generics for things like tuples, and I am reluctant to add another "open ended" form that might bring on similar complications. |
Team member @nikomatsakis has proposed to postpone this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
Fair enough. I'll bide my time, and see if I can help move RFCs that might make swallowing this RFC later easier in the meantime. I'll aim for after the 2021 edition release and see what's changed from now. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
The final comment period, with a disposition to postpone, as per the review above, is now complete. By the power vested in me by Rust, I hereby postpone this RFC. |
Add anonymous variant types, a natural anonymous parallel to enums much like tuples are an anonymous parallel to structs.
This RFC is intentionally minimal to simplify implementation and reasoning about interactions, while remaining amenable to extensions through the ecosystem or through future proposals.
Rendered
Thanks to everyone that helped me identify points that I may have missed in the Internals thread and on Reddit.