-
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
Enum variant types #2593
Enum variant types #2593
Conversation
Great work, @varkor. I've been looking forward to this for a long time. Just as a side-point, I'd love to follow this up with an RFC for the ideas in https://internals.rust-lang.org/t/pre-rfc-using-existing-structs-and-tuple-structs-as-enum-variants/7529 once this gets implemented in nightly (or perhaps even before). Since you've worked on this, would appreciate your thoughts at some point. |
This is possible in Scala - as in your example, Left and Right are subtypes of Either, and can be referred to independently. Coming from Scala, I miss this feature in Rust, and I am fully in favour of this RFC. |
Ah, great, I'll add that in, thanks! |
I think I'm in favor of the proposed functionality and semantics here. Where I'm stumbling is the nomenclature/terminology/teachability(?); it's not clear to me that "introducing a new kind of type: variant types" is the best description of this. In particular, precisely because this proposal feels so lightweight compared to previous ones, it doesn't really "feel" like what we're doing is adding a whole new type kind the way structural records or anonymous enums would be doing. It sounds like it could be equally well described as doing the "duplicating a variant as a standalone struct" workaround automagically, so those extra structs are just always there (except they get a specific layout guarantee and different conversion syntax that regular structs wouldn't get). Is there some detail I overlooked that makes this clearly not a sugar? I'm guessing this is at least partially ignorance on my part because
makes it sound like "variant types" are an actual thing with their own special properties that no other kinds of types have, and I just have no idea what that would be (since being autogenerated, having a certain layout guarantee and different conversion syntax seem like "surface level" properties that aren't really part of the type system per se). Maybe I just need to see some more examples of how these types behave? |
Nice RFC.
Is code like this still allowed, or is the compiler going to tell me that the Sum::B(b) branch of the match is impossible and needs to be removed?
Both options have advantages and disadvantages. |
This is a good question — I'll make note of it in the RFC. Although matching on variant types permits irrefutable matches, it must also accept the any other variants with the same type — otherwise it's not backwards compatible with existing code.
It's quite possible there's a better way to explain this. They are essentially as you say, though they act slightly differently from structs (on top of the points you made) in the way they are pattern-matched (as above in this comment) and their discriminant value. I thought it would be clearer to describe them as an entirely new kind of type, but perhaps calling them special kinds of structs would be more intuitive as you say. I'll think about how to reword the relevant sections. |
14a6e83
to
2b00420
Compare
This was previously proposed in #1450. That was postponed because we were unsure about the general story around type fallback (e.g, integer types, default generic types, etc). Enum variants would add another case of this and so we wanted to be certain that the current approach is good and there are no weird interactions. IIRC, there was also some very minor backwards incompatibility. This RFC should address those and issues, and summarise how this RFC is different to #1450. For the sake of completeness, an alternative might be some kind of general refinement type, though I don't think that is a good fit with Rust. I'm still personally very strongly in favour of this feature! The general mood on #1405 was also positive. |
I'm not sure that would need to be at odds with variant types, if Rust ends up with refinement types I expect variant types to be refinements of their enum. |
First, irrespective of what happens with the RFC; I am of two minds and a bit torn about the proposal here.
(Feel free to integrate any points that you found relevant into the text of the RFC)
👍
(Aside, but let's not go too deeply into this: I personally think that refinement / dependent typing is both a good idea, a good fit for Rust's general aim for correctness and type system power for library authors -- and RFC 2000 is sort of dependent types anyways so it's sort of sunk cost wrt. complexity -- the use cases for dependent/refinement types are sort of different than the goal here; With dependent types we wish to express things like
I agree; I think you can think of variant types in the general framework of refinement / dependent types; type FooVar = { x: Foo | x is Foo::Variant(...) }; |
We do want formal verification of rust code eventually, and afaik doing that well requires refinement types. I'm not saying rust itself needs refinement types per se, but rust should eventually have a type system plugin/fork/preprocessor for formal verification features, like refinement types. I do like this feature of course, but ideally the syntax here should avoid conflicts with refinement types. |
Are there any such conflicts in your view? |
(Or to elaborate; if there are any conflicts with the RFC as proposed with refinement typing, then stable Rust as is has that conflict since the RFC does not introduce any new syntax...) |
So, if I understand correctly, all existing code that uses enums now have coercions all over the place in order to ensure they continue functioning? I'm really not sure this works... let mut x:
x = None;
// At this point the compiler knows the type
// of x is Option<?0>::None.
// But Option<_>::Some cannot be coerced to None
x = Some(1); // type error? |
I think in theory the type system should infer x to be of type But I think we need a formalization of the involved type system rules, to assure soundness, before implementing this proposal... |
I would rather frame this as follows:
|
While I don't dislike LiquidHaskell-like refinement typing, lately for the future of Rust I prefer a style of verification as in the Why3 language ( http://why3.lri.fr/ , that is also related to the Ada-SPARK verification style). We'll need a pre-RFC for this. |
I hope this syntax is also supported (I suggest to add it to the RFC):
A question regarding the ABI: is the print_a1() function receiving the Sum discriminant too as argument? And in future it could also be supported the more DRY syntax (I think suggested by Centril):
You could also add a new (silly) example to this RFC that shows the purposes of this type system improvement:
With this improvement you can write instead:
Then you can define a list_head_succ() function that returns the head of the result of prepend() without a unwraps or Option result:
For the common case of integer intervals for Rust I sometimes prefer a shorter and simpler syntax like:
|
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.
"Overhead"
I'm mostly interested in this RFC from the point-of-view of "enums of lots of standalone other types". The biggest example I have is the AST expressed in fuzzy-pickles, a Rust parser which uses this pattern extensively:
pub enum Item {
AttributeContaining(AttributeContaining),
Const(Const),
Enum(Enum),
// ...
Unfortunately, I don't see this as being a large win for such a case due to the "forced overhead" of each enum variant still being the same size as all the other variants. It's an understandable decision, just not one that I see as helping as much as it could.
This is mentioned in the alternatives section, but I want to make sure the point is reiterated.
Multiple variants
I didn't see any mention of if multiple variants would be supported:
#[derive(Debug)]
enum Count {
Zero,
One,
Many(usize),
}
fn example(c: Count) {
use Count::*;
match c {
x @ Zero | x @ One => println!("{:?}", x), // what is the type of `x` here?
x => println!("{:?}", x),
}
}
It may also be worth explicitly calling out what the type is for those catch-all patterns as well as in cases of match guards.
foo @
This may be swerving into refinement type territory, but I naturally wanted to not type the foo @
in the previous example:
match c {
Zero | One => println!("{:?}", c),
// ...
I feel this is a pretty hidden and uncommon aspect of patterns, and it'd be nice to just be able to intuit the type based on the pattern without adding the explicit binding. That might even mean we could do:
if let Count::Many(..) = c {
println!("{}", c.0);
}
and `impl Trait for Enum::Variant` are forbidden. This dissuades inclinations to implement | ||
abstraction using behaviour-switching on enums (for example, by simulating inheritance-based | ||
subtyping, with the enum type as the parent and each variant as children), rather than using traits | ||
as is natural in Rust. |
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 a fan of the proposed style, but it might be worth stating why Rust the language wants to dissuade this pattern.
- Passing a known variant to a function, matching on it, and use `unreachable!()` arms for the other | ||
variants. | ||
- Passing individual fields from the variant to a function. | ||
- Duplicating a variant as a standalone `struct`. |
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 disagree that this goal is going to be as widely achieved by this RFC as I would like due to the following point:
the variant types proposed here have identical representations to their enums
That means that if I have an enum with large variants:
enum Thing {
One([u8; 128]),
Two(u8),
}
Even the "small" variants (e.g. Thing::Two
) are still going to take "a lot" of space.
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.
If space is a concern then we could have it so variant types only convert to their enum by-value, so e.g. a &Thing::Two
wouldn't be a valid &Thing
.
That's weaker than something more akin to refinement typing, but maybe it's enough?
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.
@eddyb I think that's already the case; the RFC doesn't state anywhere, as far as I can tell, that &Thing::Two
is a valid &Thing
. Also note that the RFC explicitly states that Thing::Two
and Thing
having the same layout is not a guarantee so we could change the layout to be more space efficient.
Hey Wong, nice suggestion! Those would be cool to see in Rust but Mapped Types are a feature of a more loose, structural type system. I would struggle to think of a way they would provide value in rust that a map (HashMap) doesn't provide. Conditional types are interesting but I don't think they necessarily solve the problem of "I want specific variants of this enum" or "I will only ever return specific variants of this enum," because variants are not separate types right now, they're constructors of the enum they're declared in. So as far as my understanding goes (happy to be wrong!), in order for conditional types to be useful with enum variants then they would have to first be types you can refer specifically to, which is the intent of this RFC. |
The final comment period, with a disposition to postpone, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. The RFC is now postponed. |
One really useful use case would be for adding an |
Especially true if you have lots of associated constants on a variant that you need to access in cases where you don't have an instance of the type (yet). Since this is all tied to a type anyway, having to construct an instance just seems "wrong". |
Hey 👋🏾 |
@weilbith: Someone may correct me if I'm wrong, but I don't think there's a formal answer for that besides "this is something the project would like to consider in the future but don't have sufficient bandwidth for today". Informally, I think someone will at least post a comment on this issue if there is some progress, even if that is merely linking to a new issue elsewhere. |
If someone was interested in pushing this proposal forwards, I believe it would be productive to try to implement the feature described in the RFC. This would make the interaction of the feature with the rest of the language clearer, and reveal any edge cases that might have been overlooked. The downside to this approach is that, since the proposal has not yet been formally accepted, there is a chance that, even if the feature was successfully implemented, the proposal would not be accepted. (However, given the clear interest in this feature, my impression is that it is primarily implementation questions that led to the feature being postponed.) Unfortunately, I personally do not have time to mentor such a project at the moment, though perhaps there are others knowledgeable with the type system who could offer to mentor someone through an implementation. |
Based upon work done since then, it feels like a proper solution to this wouldn't be specific to enums and instead allow arbitrary patterns. This also answers a lot of questions of the approach, like how they'd be nested. Under the syntax proposed in the linked PR, Perhaps a suitable path forward would be to propose an RFC just covering these pattern types for a very limited number of patterns, while still leaving the door open to support arbitrary pattern types in the future. |
@clarfonthey I don't think nesting needs to be addressed for this.
Then in the simple version there would only be six types, Trying to make it so there is a type that corresponds to knowing you specifically have a |
Enum variants are to be considered types in their own rights. This allows them to be irrefutably matched upon. Where possible, type inference will infer variant types, but as variant types may always be treated as enum types this does not cause any issues with backwards-compatibility.
Rendered
Thanks to @Centril for providing feedback on this RFC!
Current status of this RFC (quoted below): #2593 (comment)