-
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 name elison through inferred variant constructors #1949
Enum name elison through inferred variant constructors #1949
Conversation
|
||
The feature can also provide a slight benefit for refactoring: if the name of | ||
an enum is changed, code that uses inferred variant constructors would not | ||
need to be renamed. |
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.
And it offers a disadvantage for refactoring as well, as you now can't grep over the codebase to find out where an enum is used when you e.g. want to rename a variant. Yes, you could just grep for the variant, but usually variants have very short and generic names (Local
, Remote
, etc.), so especially in larger codebases thats not possible.
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 that not apply to imported variants as well?
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.
@phaylon when you import the variants, you'll still have to mention the name of the enum to do so. So grepping by the name of the enum will turn up that import location. And then after it you can refine your search, most often you'll just be able to ctrl+f as its all in the same file.
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.
Ah, now I understand. Personally, I see there being enough barriers to that already (import aliasing for one) that I'd hope that would be eased by cargo/RLS tooling at some point, since it is already a bit of a manual hunt. As such, I see the conveniences outweighing that for me.
|
||
The feature could encourage library writers to use domain-specific enums more | ||
often, rather than to re-purpose generic enums like `Option` or `Result`, | ||
leading to more readable code. It would also allow library writers to use |
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.
Option and Result have lots of functions on them, while custom enums don't, or first need them to be defined. They are very useful and allow for patterns like chaining.
And library authors can just declare a prelude with all of their enum variants inside. Or am I mistaken? idk...
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 big plus I see is that it drastically reduces friction for example in the case when you want to have a descriptive enum instead of a boolean flag as function argument.
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, that's an advantage. But then again, there is the builder pattern which works great as well (EDIT: so you won't need to use lots of booleans as flags to confuse the reader, but be able to make invocations like LogBuilder::new().channels(5).with_verbosity(true).create()
instead of Log::new(5,true)
).
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.
Option and Result have lots of functions on them, while custom enums don't, or first need them to be defined. They are very useful and allow for patterns like chaining.
They can be, but they are not always needed. The proposal is focused on reducing the boilerplate for “one-off” uses of enums, with a particular emphasis on users of multiple different libraries.
And library authors can just declare a prelude with all of their enum variants inside. Or am I mistaken? idk...
Preludes compound the risk of naming collisions between different libraries due to its naming pollution. They are also risky for library authors as any addition of items to a prelude can cause breakage downstream. It would also be difficult for preludes to handle multiple enums in which some of the variants have the same name.
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.
so you won't need to use lots of booleans as flags to confuse the reader, but be able to make invocations like
LogBuilder::new().channels(5).with_verbosity(true).create()
instead ofLog::new(5,true))
.
Actually a strong reason for this RFC is to replace ad hoc enums such as that. The ship has long sailed with regard to Rust’s context-sensitivity because of the .field
syntax. This is what enables “builder” patterns such as what you describe.
So in essence what I am proposing is to simply add first-class support for what folks are already doing anyway, but at least they can do it without all the boilerplate needed for the builder pattern. They can also use a significantly more pleasant syntax:
Log {
channels: 5,
verbosity: _::Debug,
}
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 ship has long sailed with regard to Rust’s context-sensitivity because of the .field syntax.
I'm a bit confused to what you refer to when you mean field syntax, as it doesnt make any sense. If you mean the ability to do .sth
on expressions and then the sth field or sth function gets accessed, then it doesn't really serves your argument, in fact its the opposite. First, it doesn't really compare to enums, and if it did, it is an example for explicitness, not against it, as you always need something to apply it to. You can't say .new()
out of the blue, you always need an expression for it. The equivalent to your proposal would be _.new()
or something, but as I said before, it doesn't really compare.
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.
First, it doesn't really compare to enums
Fields are dual to inferred variants in the following sense:
- The field syntax
obj.sth
requires the type of its argumentobj
to be known. You can’t write.sth
without the argumentobj
. - The inferred variant
receiver(_::Sth)
requires the type of its receiving context to be known (equivalently, the argument type ofreceiver
must be known). You can’t write_::Sth
without its result being received by anything (e.g.{ _::Sth; }
is a type error).
variants of an enum without ever bringing the enum or its variants into scope. | ||
|
||
This could lower the usability barrier for enums, particularly for library | ||
users. It would reduce the verbosity of enums to a level similar to |
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.
Yes, if you use some enum once inside your entire codebase, then it'll reduce verbosity. But then again I think that verbosity is justified, to help readers find out what enum it is about. However, if you use an enum very often in your mod, you can import its variants at the top of the file, and use it throughtout the file. Then writing _::Alpha
ten times is more verbose than writing use foolib::Foo::*;
once and writing Alpha
ten times (you spare 10 characters). So here its the opposite, the proposal will increase verbosity.
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.
Are you suggesting to import all enum variants? That will get ugly as soon as there are variant name clashes.
Anyway, it definitely reduces the verbosity compared to the prefix::Variant
case. And I think this is the one that is currently recommended by the style guide.
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.
That will get ugly as soon as there are variant name clashes.
When you have clashes, then its even more important to distinguish between them inside your code. Otherwise you lose readability.
And I think this is the one that is currently recommended by the style guide.
This is an RFC to propose changes to the rust language, not the official rust style. If you don't like the official style, propose to change it instead.
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.
When you have clashes, then its even more important to distinguish between them inside your code. Otherwise you lose readability.
I believe that in cases like function arguments the context provides readability. In general I agree with you though. I'm in the "import as little as possible" camp.
This is an RFC to propose changes to the rust language, not the official rust style. If you don't like the official style, propose to change it instead.
Change it to what? I see disadvantages in both "prefix all the variants" and "import the variants" styles. I feel like a solution with the functionality like this RFC is a third, in between way that hits a sweet spot.
I have not decided if I like this RFC or not. It's fine to write That said, an argument in favor of this RFC is that
I could imagine Another point that might support or oppose this RFC is that custom enums intentionally overlap sometimes, like when they represent different states at different steps in some process. I recently wrote something like :
In general, I could imagine some syntax like An an aside, can you use wildcards inside names, like |
Random thoughts, assuming
|
|
||
Inferred variant constructors do not take the namespace into consideration at | ||
all, nor do they affect the namespace. It is purely type-directed, albeit | ||
subject to visibility rules. |
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 think we need more detail on what these last six sentences mean (i.e., the actual rules of how this feature works). They sound precise, but when I asked myself "is this RFC introducing a (new?) form of ADL?" and tried to find the answer here, I honestly couldn't tell if this wording is deliberately introducing ADL or deliberately avoiding it, though it sounds like it's trying to do one of those things.
To save non-C++ people from reading that link: it's not at all clear to me whether this RFC would require a use Foo;
or use Foo::*;
statement in the same file or the same module or not at all in order for _::FooVariant
to type check.
Simply adding compiles/doesn't compile code samples under each of these statements would help tremendously.
(personally I'm not a fan of this RFC because I just don't see the advantage of the _::
operator over a simple use MyEnum::*;
statement, but in the process of trying to articulate why I realized I don't even understand what the proposed behavior of _::
is)
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 not ADL for several reasons:
- One is that it never takes namespace information into account, only type information.
- The other is that it considers the type of the receiving context, not the type of the argument.
I’ll add examples to clarify the behavior.
I just don't see the advantage of the
_::
operator over a simpleuse MyEnum::*;
statement
use MyEnum::*
pollutes the local namespace, whereas _::
does not. It also requires adding a use
anywhere between a few lines above or at the top of the module, whereas _::
maintains locality.
Edit: Submitted too early.
It’s works, but I see that as more of a workaround for a missing feature than a proper solution. It would require inventing a letter for it even though the type system knows very well what the enum should be. The point of this RFC is to reduce the amount of
Haskell’s ergonomics with sum types (and records) is quite poor IMO. In Haskell sum types bleed into the surrounding scope so one has to either define them in their own separate files and then import them qualified or use prefixes like Rust, unlike Haskell, has been historically quite bold with additions to its type inference, even if it meant the user would have to add type annotations more often. The
I don’t believe Rust does that.
The
It could also break existing (albeit non-idiomatic) code that use |
I see. You cannot bring |
@killercup Why not? Seems like a sweet alternative. |
Though I am less opposed to |
What about |
I came in here from IRLO thinking that What about using (I'd be tempted to say just Analogous future work: If this uses |
@comex that will get in the way of type ascription. |
and |
I find it useful having the full name of an enum, in particular it allows for a nice pattern of naming without prefixing, e.g.,
Now wherever I use this, my code is self-documenting. If the
So I end actually having to type almost as much with elision as without and, the enum decl gets uglier. |
@est31 I don't think it would interfere with type ascription, as type ascription colons appear in the 'after expression, expecting binary operator' state, while this colon would appear in the 'expecting expression' state. (Rust's grammar already has constructs that parse differently depending on this state: |
Wouldn't this make more things into breaking changes that previously weren't? Suppose I write |
@joshtriplett I think the idea is that this elision only works when the needed type can be inferred from what the result is supposed to be. So |
@joshtriplett To add to what @est31 has said, the proposed inference mechanism does not take into consideration what exists or doesn't exist in the current scope. |
@nrc I don't think this code is particularly unclear: match self.supports_foo {
_::Yes => { ... }
_::No => { ... }
} If your variable is named well, we'll know what the elided type is. If not, chose not to elide the type. That said, I'm not convinced this is better than just |
Just backing up for a moment : You could use inference to give a warning instead of an error on say As an aside, I've kinda wanted to place a
|
Those are the reasons. In general it would be best to avoid sensitivity to the availability of identifiers in the current scope. The proposal is intended to be analogous to |
Here are the proposed (unambiguous) syntaxes so far:
Let me know if I missed someone or any pros/cons. This was mentioned in the RFC a few times, but it might be worth elaborating on the commonalities between inferred variants and field expressions. Since I'm not committed to any particular syntax, I'll use Suppose Rust didn't have field expressions:
Now consider the Rust we have now:
What about the current situation without inferred variants?
With the proposal:
It has also been argued that field expressions alone are sufficient to emulate inferred variants through the builder pattern. But:
Therefore, inferred variants are an orthogonal feature from an inference standpoint. Are the advantages of this feature significant? IMO, not any more than field expressions, if one sets aside the fact that Rust was very much built around field expressions. Naturally a lot of Rust's features are entangled with it, but it would have been entirely possible to design the language without field expressions. After all, Haskell is a perfectly productive language even without the ⁺ Removal of "or construct it" is a candidate for another proposal. |
If these matches are done in methods of an enum, you can always use I personally do |
Could I write
|
Speaking as both a swift and rust user, the |
@burdges I was trying to suggest a more concise alternative. But yes. |
Inner enum problemWhile thinking up an alternative proposal I ran into a problem and I realized @joshtriplett's idea has it, too. It's not satisfying to only pull in variants of the outermost enum, because I often want to omit enum name prefixes in subpatterns as well, like so: use path::to::OuterEnum::*;
use path::to::InnerEnum::*;
match e {
OuterVariant(InnerVariant) => ... So I would only support a proposal that solves this inner enum problem.
|
I think we could also parse this syntax, saving a vertical line: match foobar use OuterEnum, InnerEnum {
OuterVariant(InnerVariant) => ... match rfc.error use RfcError {
SyntaxNotGoodEnough => self.generate_syntax_idea(&mut rfc), |
Rather than coming up with new syntax for this we could just only do this sort of name resolution if the expression is type ascribed: match x: SupportsFoo {
Yes => { }
No => { }
} All of these ideas seem like a layering violation, but at least this isn't any new features. |
@withoutboats I don't think that's good enough (inner enum problem again). |
That said, I wanted to get my idea in front of some other people, but I'm also pretty comfortable just doing nothing about this. |
@solson since the type is fully concrete, you can see through inner enums (though they won't be named locally). That is, you know that |
@withoutboats Oh, I didn't realize it was going to implicitly pull in all nested enums... hmm. Is that too surprising? |
@solson It could or it couldn't. I don't think I'm in favor of any of this vs just writing normal |
For the |
If the type ascription approach does not cause problems elsewhere, then one should consider how far that can be pushed outside of I donno if I think the As an aside, I suppose the |
This @solson's solution - #1949 (comment) - is probably the least invasive solution I can imagine. I still disagree with the motivation though because I personally use (new style) variants in qualified form only |
Comments #1949 (comment) and #1949 (comment) still reflect the opinions of the language team towards this problem. On that basis: @rfcbot close |
@rfcbot fcp close |
Team member @nrc has proposed to close this. The next step is review by the rest of the tagged teams: No concerns currently listed. Once these reviewers reach consensus, 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. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
This is something I've wanted, and I agree that
I don't think this is the case. As @withoutboats points out, in many cases it will be clear from the context, anyway, and one can always choose to include the enum name if it makes the code clearer. |
The final comment period is now complete. |
Closing as per FCP. To be clear, there is a genuine ergonomics issue here, and it's a problem that is certainly within the purview of this year's productivity roadmap, but the design here doesn't quite hit the sweet spot. If you have more ideas on this topic, or want to follow up on one of the linked comments with a pre-RFC, please feel free to open an internals thread on it, or reach out to me (or others) on the lang team for mentoring. |
Thank you all for reviewing this! |
Since match res.status() {
tide::StatusCode::NotFound => Ok(tide::Response::new(tide::StatusCode::NotFound)),
tide::StatusCode::InternalServerError => Ok("Something went wrong".into()),
_ => Ok(res),
} can be written as match res.status() in tide::StatusCode {
NotFound => Ok(tide::Response::new(NotFound)),
InternalServerError => Ok("Something went wrong".into()),
_ => Ok(res),
} It can also be used for matching any statics in a module path: match value in mycrate::mymod {
ONE => 1,
TWO => 2,
_ => 3,
} if we have defined pub static ONE: usize = 0;
pub static TWO: usize = 1; |
Sad that this is not in Rust yet. One of the best features in Swift and would love to see it in Rust. |
As far as I can see, the problem basically lies in possible confusion/overlap with module syntax. Makes me wonder... why was |
Rendered
Allow the name (qualifier) of an enum variant to be elided in expressions and patterns whenever it can be inferred.
(Previous forum discussion)