-
Notifications
You must be signed in to change notification settings - Fork 1.7k
RFC: Open up your enum with an unnamed variant #3894
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
base: master
Are you sure you want to change the base?
Conversation
There is no reason for these bytes to be in flash memory - it is merely one of the applicable use cases.
unnamed_variants is an unambiguous feature name that describes what the author is now able to spell. However, it takes more mental effort to remember what a variant is, while "enum variant" is immediately clear. This also clarifies the zero-copy deserialization motivation language further.
|
Out of curiosity, could the following syntax work? (or something similar) #[repr(u32)]
#[reserve_discriminants(5..10)]
#[reserve_discriminants(15..20)]
enum Fruit {
Apple, // Apple is represented with 0u32.
Orange, // Orange is represented with 1u32.
Banana = 4, // Banana is represented with 4u32.
} |
See f17e862 for reasoning.
The `taken_discrimimant_ranges` and `empty_discriminant_ranges` lints catch the situation in which an unnamed variant has no effect. The latter is much more dangerous than the former, so it is `deny`-by-default. However, both should be allowed for certain codegen and macro cases as described. This also moves around and expands some language regarding `derive` difficulty in open-enum, as well as add a suggested alternative.
|
@nielsle Using an attribute to reserve discriminants/open an enum (of which there are many spellings) is discussed here as a considered alternative. Also, please use inline (file/line-level) comments instead of PR-level comments for RFC comments in the future to keep the discussion organized. |
Fundamentally I think I disagree with this. A C-style open enum is a newtype struct, not a rust If the main complaint is that you don't like how rustdoc shows that or how R-A generates things for it, I think it'd be better to start with tooling changes for that instead of language changes. How much of this would be solved if you could just have a And I think that "well I didn't want to reserve all of the values" both
|
| - The unstable [`non_exhaustive_omitted_patterns`] lint has no easy way to | ||
| work with this enum-alike, even though treating it like a `non_exhaustive` | ||
| enum would be more helpful. | ||
| - Generated rustdoc is less clear (the pseudo-enum is grouped with `struct`s). |
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.
TBH, I've long found this distinction in rustdoc unhelpful always. If it's an opaque type, for example, what do I care if it's a struct or a union? Not to mention that I'd rather have Statement and StatementKind next to each other on https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/ rather than multiple pages apart.
I'd absolutely 👍 an RFC to change rustdoc to group things in a module by namespace instead of item kind, but I don't think the current behaviour is a reason to do a language change.
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.
(Using colour or an icon or something for struct vs enum to help give my brain an extra cue as I scan? Sure, great! I just don't think it makes sense as a top-level split.)
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 would love to have more control over rustdoc presentation and organization of types as well, in general. I consider the accurate grouping in rustdoc for an open enum as more removing a papercut than a full motivation for the 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.
Would you like me to change the RFC text to reflect that a hypothetical future change to rustdoc could negate this particular issue?
text/3894-unnamed-variants.md
Outdated
| - `derive`s cannot be easily written with enum variant names. In order to avoid | ||
| duplicating the names, a `derive` macro must directly inter-operate with | ||
| another macro that generates these pseudo-variants like `open-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.
I didn't understand this. Can you clarify? What duplicate names are you referring to? What's an example derive macro that would be difficult to implement with this attribute-based scheme?
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 suppose one issue I can foresee is that a derive macro can't enumerate the known variants with this approach. So e.g., you can't implement Debug or Display or similar via a derive macro on such a type--those would have to be provided by a declarative or attribute macro.
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.
The duplicate names I'm referring to are the variant names - the impl block contains the names, but a derive macro needs to be able to see it on the struct definition in order to work with the names. See this comment.
text/3894-unnamed-variants.md
Outdated
|
|
||
| However: | ||
|
|
||
| - Open enums require even more typing for the desired semantics. |
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 is trivially solved with a macro, right?
Do you expect that this open enum pattern is common outside of specialized and generated code? The motivating examples of bindgen and protobuf are both generated so there is no "typing".
(I personally use open enum patterns in a lot of systems code for ABI types that cross trust boundaries. But I am generally OK with reaching for a macro for these use cases, which I would consider fairly specialized.)
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 is trivially solved with a macro, right?
"trivially" comes with a big asterisk, as described in this comment. What about non-literal discriminant expressions and implicit discriminants? It's not as trivial as it initially seems.
I'm aware that a macro solves the "extra typing" problem, as is mentioned repeatedly throughout the text and in a following bullet point. It is still a drawback to require a macro in order to avoid extra typing or duplicating discriminant values/variant names. Can you suggest alternative language that is less confusing to you?
Do you expect that this open enum pattern is common outside of specialized and generated code? The motivating examples of bindgen and protobuf are both generated so there is no "typing".
I expect the pattern to be more common once users realize they can as cast from the backing integer (lovely!) and improve some benchmarks by shifting validity checks around without sacrificing too much on ergonomics (that extra match arm). Also, folks should be using bindgen but they often don't - this makes it easy for them to get the open enum aspect right.
|
This RFC is about more than open enum ergonomics - it's about empowering Rust users to control ABI compatibility for enums.
I'm curious - why do you think that a C-style enum is a newtype struct and not a Rust The choice of An open
That's very unlike a A newtype integer is a fine way way to represent a C-style open enum, but is an irritating paradigm in practice. My goal is to vastly improve the user experience for those who handle open enums regularly. Having this be a language feature is friendlier to users of open enums, because a solely diagnostic improvement will always require using a 3rd party macro or duplication in order to avoid significant boilerplate. Consider this enum that we want to be made open with an #[open_enum]
#[repr(u32)]
enum Foo {
A = 0,
B = 1,
C = 5,
D = 7,
E = 9,
F = 10,
G = 20,
H = 21,
}That converts #[derive(ParitalEq, Eq)] // for use in `match`
struct Foo(pub u32);
// plus checks to ensure no duplicate discriminants
#[allow(non_uppercase_globals)] // n/a
impl Foo {
const A: Foo = Foo(0);
const B: Foo = Foo(1);
const C: Foo = Foo(5);
const D: Foo = Foo(7);
const E: Foo = Foo(9);
const F: Foo = Foo(10);
const G: Foo = Foo(20);
const H: Foo = Foo(21);
}Myself and clients have run into some issues with this pattern in practice:
There's a solution to this, but it still has serious drawbacks. With a new design, the #[open_enum]
#[derive(open_enum::Debug, open_enum::FromStr)]
#[repr(u32)]
enum Foo {
/* variants A-H */
}into this: // `open_enum` derive macros read `open_enum_variant_names`, as well as
// other macros that are aware of the attribute.
#[derive(open_enum::Debug, open_enum::FromStr, PartialEq, Eq)]
#[open_enum_variant_names = "A,B,C,D,E,F,G,H"]
struct Foo(pub u32);
impl Foo { /* ... */ }With this:
This is similar to the If a Also, for feature parity,
I'm aiming to improve the text - can you be more specific? Rust users have good reasons to create enums with 1010 reserved variants, and it's not great. It's not the first time I've seen an enum like that (and won't be the last).
I disagree - the ergonomics of enums with unnamed variants cannot be simply solved with an attribute, and this applies to Pattern types and unnamed variants cooperate well - if you write |
Another problem with that is it wouldn't really allow having a lint to check that a match includes all known variants. |
|
Nice RFC! I want to add few additions that I think are quality of life improvements:
|
| - Crates must be recompiled to use new enum variants. | ||
| - It affects _only_ foreign crates. | ||
|
|
||
| By contrast, an unnamed variant affects API _and_ ABI semver compatibility: |
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.
Why are we requiring API semver compatibility? In use cases like protobuf, I would like to correctly handle unknown wire variants, while still having the compiler check that my code matches all the "known" variants in the current API version.
It seems to me that ABI and API compatibility are actually orthogonal, and I can't see any reason it would be desirable to bundle in the API part to this feature when that's what #[non_exhaustive] already does.
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.
having it affect API is necessary to handle stuff like:
#[repr(u8)]
enum E {
A = 0,
B = 1,
_ = ..,
}
match 4u8 as E {
E::A => String::from("A"),
E::B => String::from("B"),
// no other match arms, what does this do?
// the only reasonable option is a compile error.
// that's why you need a wildcard match arm
}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 do think there should be lint that triggers if you don't explicitly handle one of the known variants.
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.
Yeah a lint might be sufficient. Alternatively I guess there could be special syntax to match unnamed variants, distinct from the generic wildcard which matches named variants.
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 do think there should be lint that triggers if you don't explicitly handle one of the known variants.
there is a nightly lint non_exhaustive_omitted_patterns which you can change to warn when _ matches any enum variants in a #[non_exhaustive] 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.
there is a nightly lint
non_exhaustive_omitted_patterns
Indeed, that's the desired lint. The interaction of this lint with unnamed variants is described in the RFC and allows a maintainer to ensure that a match doesn't forget to list a named variant. As mentioned there, it could be renamed before stabilization if the name mismatch seems confusing. I don't think it makes sense to separate into a different lint.
#[repr(u32)]
enum Bar {
A,
B,
_ = ..,
}
let b = Bar::A;
// warning: some variants are not matched explicitly
// pattern `Bar::B` not covered
// help: ensure that all variants are matched explicitly by adding the
// suggested match arms
// note: the matched value is of type `Bar` and the
// `non_exhaustive_omitted_patterns` attribute was found
#[warn(non_exhaustive_omitted_patterns)]
let name = match b {
Bar::A => "A",
_ => "unknown",
};I guess there could be special syntax to match unnamed variants, distinct from the generic wildcard which matches named variants
It's quite intentional that unnamed variants make naming a previously unnamed variant a non-breaking API change, which a special syntax for matching on named/unnamed variants would violate. However, I do think it's occasionally useful to be able to query whether a value is a named variant, and so this operation is trivial to generate with a macro. It's equivalent to a matches! on a | pattern that lists every named variant. Macros are allowed to cause a non-breaking API change to a type's structure yield a breaking API change; see derive(PartialEq) and anything else that works per-struct-field.
The syntax would thus be something like:
#[repr(u32)]
#[derive(IsNamedVariant)]
enum Bar {
A,
B,
_ = ..,
}
let b = Bar::A;
fn known_value(b: Bar) -> Option<u32> {
x.is_named_variant().then(|| x as u32)
}
assert_eq!(known_value(0 as Bar), Some(0));
// If you add `C` with discriminant 2, this panics.
assert_eq!(known_value(2 as Bar), None);I think it's best to leave this functionality to 3rd-party macros.
I can't see any reason it would be desirable to bundle in the API part to this feature when that's what
#[non_exhaustive]already does
I describe my reasons for having this replace non_exhaustive rather than require it to use unnamed enum variants here.
Based on [this comment](rust-lang#3894 (comment)).
| variant by changing the `repr`. This is non-obvious and can be avoided by | ||
| forbidding `non_exhaustive` when a valid unnamed variant exists. | ||
|
|
||
| ### Allow an implicit discriminant expression for unnamed variants |
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.
Nice RFC!
Thanks @tmvkrpxl0 😄
Please reply in this inline comment to keep the main thread cleaner, or resolve the thread.
_variant without pattern assigned to it should be same as_ = ..
I think that's ambiguous - why wouldn't it be equivalent to _ = <previous variant + 1? Unnamed enum variants are unusual enough that I think making it clear that discriminants are involved should be mandatory.
- If it's used without
#[repr], it should behave same as#[non_exhaustive].
I think this adds more confusion than improves ergonomics. I don't think it makes as much sense to talk about assigning "the rest" of the discriminants to an enum without a repr as _ = .. says.
- Given that it isn't breaking change to replace
non_exhaustive, it already work as its replacement. If it cannot guarantee ABI compatibility due to missing#[repr], it should instead ensure API compatibility only, effectively replacing it.
It doesn't replace non_exhaustive, because it can also be applied to structs and enum variants. The unnamed fields RFC also requires repr(C) in order to be used since it's about ABI.
There should be way to match for only known enum variants or only unknowns
- I can see someone printing warning when enum value isn't known.
See this reply - there is both an existing nightly lint to warn about unlisted named variants, and getting a bool for whether a variant is named (for a match guard) can be trivially done with a macro.
To me this feels like a fixation on a particular UI for it. You could always have an |
| [rest pattern]: https://doc.rust-lang.org/reference/patterns.html?#rest-patterns | ||
| [wildcard pattern]: https://doc.rust-lang.org/reference/patterns.html?#wildcard-pattern | ||
|
|
||
| ### An "other" variant carries unknown discriminants like a tuple variant |
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 is a similar alternative. Something like:
#[repr(u32)]
enum IpProto {
Tcp = 6,
Udp = 17,
#[catchall]
Other,
}Bikeshed on the exact syntax of the attribute, and whether and how it is possible to specify a specific range.
Possibly also allow including a tuple or struct field to capture the discrimenant in pattern matching.
To address the possibility of creating that variant with the discrimenant of another value, I can think of a few ways of addressing that:
- Forbid direct construction of the Other variant. You can only get it as a result of casting an integer to it.
- Make construction of it unsafe, and the caller must be responsible for gauranteeing the value is ok.
- Don't have a field for the discrimenant, and require specifying a value to use when constructing directly, but casting from an integer can result in a different discrimenent. One downside is this may not work for some use cases if there isn't a reserved value for an unknown discriment
- Combine 1 and 2. Only allow direct construction if a specific discriment is specified.
I think currently, this alternative, especially with option 4 for construction is currently my favorite syntax for open enums.
I'll also point out one advantage of using a named variant like this is that it provides a clear distinction between matching against an unknown discriment and a wildcard match, and we get an error for missing a variant in a match for free.
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 is more like the Discriminant ranges for named variants instead of unnamed variants alternative than the alternative this comment is centered on, though yours uses an attribute instead of an explicit discriminant range syntax.
My primary concerns with allowing a named variant to represent multiple discriminants, even with options 1-4, are:
- Possibly confusing or non-performant behavior of
derive(PartialEq). - Adding a variant becomes an API-breaking change (whether this represents an ABI-breaking change is debatable).
- The performance of
matches!(x, IpProto::Other)grows non-trivially as further variants are added toIpProto; no other single pattern has this characteristic today and I think it's valuable to keep it that way. - I think it is a larger change to the language than unnamed variants.
- I've not come up with a satisfying syntax that makes option 4 workable.
The utility that specifically allowing a named "opening" variant has could be provided with macro-generated functions that match self as u32, such as the derive(IsNamedVariant) example below.
and we get an error for missing a variant in a match for free
It'd be more consistent in the language if the solution for not accidentally missing a variant were the same as for non_exhaustive (i.e. use warn(non_exhaustive_omitted_patterns) or whatever stabilizes like it).
it provides a clear distinction between matching against an unknown discriment and a wildcard match
Is it actually a good thing for code to automatically take a different branch when a new variant is added? A large point of this is to make adding a variant non-breaking to code that handles the enum ordinarily. I think it should take a macro looking at the enum definition in order for code to directly observe a new named variant being added.
I'm fond of Swift's solution to this, which is to annotate the "unknown discriminant" branch with @unknown in an exhaustive switch.
It allows a clear warning when missing a variant in a match and distinguishes whether a new variant has been added, but doesn't allow a change in control flow when a new variant is added without due consideration by the author of the switch.
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 reworded and expanded the bullet points for the relevant alternative. Does that effectively reflect your proposal?
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 is more like the Discriminant ranges for named variants instead of unnamed variants alternative
True. I actually originally intended this to be with a field using the discriminant, then realized that the discriminant could also be obtained by casting, so that wasn't entirely necessary, but could be added on.
Possibly confusing or non-performant behavior of derive(PartialEq)
If by possibly confusing, you mean that IpProto::Other doesn't necessarily equal another instance of IpProto::Other, then sure. I guess that is potentially confusing, but I don't think that is any more confusing than having an enum be a value that isn't any of the variants.
Adding a variant becomes an API-breaking change (whether this represents an ABI-breaking change is debatable).
Only if the enum isn't also annotated with #[non_exhaustive]. And it is possible that making that a breaking change is desirable. For example, suppose you want code calling the library to be be aware of all known variants, and handle them, but it's possible to get an unknown variant from the network or a file, etc.
The performance of matches!(x, IpProto::Other) grows non-trivially as further variants are added to IpProto; no other single pattern has this characteristic today and I think it's valuable to keep it that way.
Only if the enum is sparse.
And on the other hand, this means there is a way to easily check if a value an unknown variant, without needing a separate derive macro.
I've not come up with a satisfying syntax that makes option 4 workable.
#[repr(u32)]
enum IpProto {
Tcp = 6,
Udp = 17,
#[catchall]
Other = 0,
}??
or
#[repr(u32)]
enum IpProto {
Tcp = 6,
Udp = 17,
#[catchall(default=0)]
Other,
}The utility that specifically allowing a named "opening" variant has could be provided with macro-generated functions that match self as u32, such as the derive(IsNamedVariant) example below.
That's an option, but it has some downsides. In particular, it requires deriving the trait on the definition of the enum. So if you are using a crate that didn't do that, then you wouldn't have an equivalent functionality, unless you defined such a function by hand (or we got macros that had some form of compile-time reflection).
It's also a little more awkward to use in some situations, for example:
match x {
Variant1 => todo!(),
other if !other.is_named_variant() => todo!(),
_ => todo!(),
}but that's a minor issue.
It'd be more consistent in the language if the solution for not accidentally missing a variant were the same as for non_exhaustive (i.e. use warn(non_exhaustive_omitted_patterns) or whatever stabilizes like it).
Is it actually a good thing for code to automatically take a different branch when a new variant is added?
What I'm getting at is that API compatibility and ABI compatibility are kind of independent of each other. #[non_exhaustive] makes an enum's API forward compatible. This RFC is trying to make it so you can have enums that have a forward compatible ABI. In practice, you probably want the API to be forward compatible most of the time when you want the ABI to be forward compatible. But there isn't any technical reason that has to be the case. Perhaps there isn't any real use case for an open enum that isn't non_exhaustive, but how confident are we that is the case?
For being able to branch on them differently, I could see a case where you may want to print different error messages. Or maybe the catchall value actually has information in the bits of the "discriminant". etc.
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.
TL;DR: this alternative introduces far more inconsistencies and API-hazards to the language than existing constructs. Unnamed variants have fewer design questions with dissatisfying answers.
When I first started on this RFC, it was this alternative. It does feel nice to be able to explicitly match on the "other" variant! It was only after breaking down the issues and comparing to other Rust constructs that I decided that unnamed variants were the less hazardous choice.
Say we only implemented this alternative, and didn't implement unnamed variants. What should bindgen call the "Other" variant on generated enums? What happens if that name already exists in the enum?
IpProto::Otherdoesn't necessarily equal another instance ofIpProto::Other
Yes, I think that it is confusing for this to be possible: matches!(o, IpProto::Other) && o != IpProto::Other. If instead of comparing discriminants derive(PartialEq) behaved like match, then we risk two linked libraries giving different answers to PartialEq::eq (ignoring the API-compatibility issue). In the presence of dynamic linking and inlining it could be very hard to predict what the actual behavior would be.
I don't think that is any more confusing than having an enum be a value that isn't any of the variants.
I disagree - I wouldn't describe the enum value as "not any of the variants", but rather "some unnamed variant". That same variant may be named in another linked library. You can declare multiple const _: T next to each other - they don't conflict because they're both unnamed.
And on the other hand, this means there is a way to easily check if a value an unknown variant, without needing a separate derive macro.
I really think the separate derive macro should be required. For every construct in the language today, it requires a macro in order for a non-breaking change to a non-exhaustive type definition to cause this sort of branching change. It would be inconsistent to change that.
Only if the enum isn't also annotated with
#[non_exhaustive]. And it is possible that making that a breaking change is desirable. For example, suppose you want code calling the library to be be aware of all known variants, and handle them, but it's possible to get an unknown variant from the network or a file, etc.
What I'm getting at is that API compatibility and ABI compatibility are kind of independent of each other
...
there isn't any real use case for an open enum that isn't non_exhaustive
I don't think they are independent; they're deeply intertwined. An ABI-forwards-compatible change must also be API-forwards-compatible. An open enum must have non_exhaustive-like properties in all crates when doing an exhaustive match (except where every discriminant is named). Otherwise, there may be no branch to take for unnamed variants.
So if you are using a crate that didn't do that, then you wouldn't have an equivalent functionality, unless you defined such a function by hand
That is a good thing, if less convenient for particular use cases. An enum author should have to explicitly opt-in to the API-compatibility dangers that using the macro represents. Perhaps static reflection will change that?
I could see a case where you may want to print different error messages.
Is it too burdensome to require the enum author opt-in with a derive or the user wanting bespoke behavior to write their own match? If there's a known range of discriminants that have names, then matching on the discriminant values would work fine.
Or maybe the catchall value actually has information in the bits of the "discriminant". etc.
In that case it would be weird for the enum author to not derive or have some other pub method to get those bits.
I've not come up with a satisfying syntax that makes option 4 workable.
#[repr(u32)] enum IpProto { Tcp = 6, Udp = 17, #[catchall] Other = 0, }?? or
#[repr(u32)] enum IpProto { Tcp = 6, Udp = 17, #[catchall(default=0)] Other, }
What I meant was, what is the syntax to construct a value of IpProto::Other? The best I had was x as IpProto::Other where x is a checked const value or dynamic pattern type.
I added another option to here to reflect what you spell above - the enum author opting in to a particular default discriminant. Using the syntax currently described in the alternative, it'd be something like:
#[repr(u8)]
enum IpProto {
Tcp = 6,
Udp = 17,
#[default_value = 0]
Other = ..,
}
// Throws an error unless `#[default_value]` is specified on `Other`
assert_eq!(IpProto::Other as u8, 0);It's also a little more awkward to use in some situations
Personally, I think that example has better local readability than what the alternative would do. We read code much more often than write it:
match x {
Variant1 => todo!(),
// How am I supposed to know that this is special and has multiple discriminants?
// It could be a particular reserved value that means "unknown".
// Who looks at type definitions? ;-)
Other => todo!(),
_ => todo!(),
}The performance of matches!(x, IpProto::Other) grows non-trivially as further variants are added to IpProto; no other single pattern has this characteristic today and I think it's valuable to keep it that way.
Only if the enum is sparse.
I'm expecting sparse enums to be a relatively common case. Rust is a systems language: there should be no surprises in the performance of something like match.
|
@scottmcm I made this Zulip thread to discuss open-enum library design further. |
| callsite. Thus it may be worth adding to Rust even if `.0` isn't. | ||
| - When should one prefer the constructor over the `as` cast? Always? | ||
|
|
||
| #### Discriminant field access |
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 have been a few other proposals for discriminant access syntax. @scottmcm has made a couple, in particular. I'd want to see those captured as alternatives here. .0 is cute but entirely too clever for its own good. :)
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.
.0 is definitely peculiar. Alas, I had to mention it- it would make this particular transition so smooth!
Thank you for letting me know about the prior art. I've been having trouble finding those - would you or @scottmcm mind linking?
| Green = 1, | ||
| // Must specify a non-overlapping range, | ||
| // including if `..` is the discriminant. | ||
| Unknown = 2..=50, |
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.
Having to explicitly exclude other variants seems onerous. If we're going to allow .. rather than (for instance) 2.., then it doesn't seem unreasonable to allow ..=50 as well. Detecting issues with that seems like the domain of a lint.
That said, I agree that ranges other than .. can go into future work in the first place; .. seems like by far the most important case.
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.
- The reason why this Future possibility excludes named variants from overlapping other variants is because it resolves most of the issues described in this comment and section of the RFC.
- This Future possibility is specifically about named variants with a range of discriminants.
- This RFC does describe the behavior of
2..and..=50for unnamed variants. - I agree that
..is by far the most important case, though it's not significantly more work to support smaller ranges. As you say, most of the work is figuring out when you should lint on possibly-confusing cases.
Enable ranges of enum discriminants to be reserved ahead of time with an unnamed enum variant. This requires all users of that enum to consider those values as valid - including within the declaring crate.
This is useful for any situation in which recompiling an enum to handle new discriminant values is infeasible, and using a newtype integer is unergonomic due to the type's intended nature as a set of enumerated values. This includes FFI, Protobuf, and embedded syscalls.
If an enum is valid for every discriminant (it has no niches), it is an open enum and may be
ascast from its explicit backing integer. For example:While writing this RFC I was not aware of the recent work on a similar RFC by @madsmtm. I considered using an attribute to make an open enum before I had learned about this RFC - I explain why I chose unnamed variants instead in the Alternatives.
@rustbot label T-lang
Important
When responding to RFCs, try to use inline review comments (it is possible to leave an inline review comment for the entire file at the top) instead of direct comments for normal comments and keep normal comments for procedural matters like starting FCPs.
This keeps the discussion more organized.
Rendered.