-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Create enum-like-unions.md #9711
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
Open
MadsTorgersen
wants to merge
2
commits into
main
Choose a base branch
from
madst-unions
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+167
−0
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
167 changes: 167 additions & 0 deletions
167
meetings/working-groups/discriminated-unions/enum-like-unions.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
# Enum-like unions | ||
|
||
## Summary | ||
|
||
**Enum-like unions** are "enum-like" in the same sense that field-like events are "field-like": They are introduced by the same keyword (`union`/`event`), and to the consumer they are the same kind of thing (unions/events), but their body uses an alternative, terser, syntax that is similar to another kind of declaration (enums/fields): | ||
|
||
```csharp | ||
public union Gate { Locked, Closed, Open(float percent) } | ||
``` | ||
|
||
## Motivation | ||
|
||
For straightforward "discriminated union" scenarios, it is desirable to have a very terse syntax for declaring fresh case types, and it is helpful to lean into enum syntax (curly braces, simple names, commas) to manifest the analogy. Both these claims are borne out by other programming languages. | ||
|
||
However, to a consumer such unions aren't observably different from ones declared with type references, just like field-like events aren't observably different from other events. Sharing the `union` keyword makes the shared semantics clear. | ||
|
||
At the same time, this lets us allow the same enum-like bodies in closed hierarchies if we want, creating symmetry between the two kinds of closed type declarations. | ||
|
||
Finally, while this proposal separates "struct-like" and "enum-like" declarations, it leaves open the possibility of unifying them. | ||
|
||
## Detailed design | ||
|
||
### Syntax | ||
|
||
```antlr | ||
union_declaration | ||
: attributes? struct_modifier* 'partial'? 'union' identifier type_parameter_list? union_declarator | ||
; | ||
|
||
union_declarator | ||
: struct_like_union_declarator | ||
| enum_like_union_declarator | ||
; | ||
|
||
struct_like_union_declarator | ||
: '(' type (',' type)* ')' type_parameter_constraints_clause* | ||
(`{` struct_member_declaration* `}` | ';') | ||
; | ||
|
||
enum_like_union_declarator | ||
: type_parameter_constraints_clause* `{` enum_like_union_member_list `}` | ||
; | ||
|
||
enum_like_union_member_list | ||
: enum_like_union_member (',' enum_like_union_member)* (`,`)? | ||
; | ||
|
||
enum_like_union_member | ||
: identifier (`(` parameter_list? `)`)? | ||
; | ||
``` | ||
|
||
Note that the `struct_like_union_declarator` shown here just reflects the current plan of record, but could change as part of other decisions. Its exact shape is not part of this proposal. | ||
|
||
### Semantics | ||
|
||
Enum-like unions are translated into struct-like unions, where enum-like union members are translated into nested record declarations (with primary constructor parameter lists if they contain parameter lists `(...)`) and added to the resulting unions case type list. | ||
|
||
For example: | ||
|
||
```csharp | ||
public union Gate { Locked, Closed, Open(float percent) } | ||
``` | ||
|
||
Translates to: | ||
|
||
```csharp | ||
public union Gate(Gate.Locked, Gate.Closed, Gate.Open) | ||
{ | ||
public record class Locked; | ||
public record class Closed; | ||
public record class Open(float percent); | ||
} | ||
``` | ||
|
||
### Examples | ||
|
||
```csharp | ||
public union Pet | ||
{ | ||
Cat(string Name, string Personality), | ||
Dog(string Name, bool Friendly), | ||
Bird(string Name, string Species), | ||
None | ||
} | ||
|
||
public union Option<T> | ||
{ | ||
None, | ||
Some(T value), | ||
} | ||
``` | ||
|
||
MadsTorgersen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
## Drawbacks | ||
|
||
- Is it too subtle that the presence or absence of a list of case type references determines whether the `{...}` body is enum-like or struct-like? | ||
MadsTorgersen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- Does the `enum` keyword need to be present to stress the analogy to enums? | ||
|
||
- Like enums, enum-like unions cannot declare struct members such as function members or nested types. Their body is reserved for case members. | ||
|
||
- Types declared as enum-like union members cannot declare their own bodies. This represents quite a cliff, as doing so for even one member requires the whole union to be rewritten as a struct-like union. | ||
|
||
|
||
## Alternatives | ||
|
||
- Use other keywords or additional keywords to further differentiate an enum-like union declaration from a struct-like one. | ||
|
||
## Additions | ||
|
||
- Allow closed hierarchies to also have an enum-like body. That way, the author of a "discriminated union" can freely choose whether to implement it as a union or a closed class without paying a syntactic penalty either way: | ||
|
||
```csharp | ||
public union Gate { Locked, Closed, Open(float percent) } | ||
// or | ||
public closed class Gate { Locked, Closed, Open(float percent) } | ||
``` | ||
|
||
- Allow the `enum_like_union_member_list` to be followed by a `;` and a list of `struct_member_declaration`s so that enum-like unions also can have e.g. function members. | ||
|
||
```csharp | ||
union Pet | ||
{ | ||
Cat, | ||
Dog, | ||
Bird; | ||
|
||
public bool HasPaws => this is Cat or Dog; | ||
} | ||
``` | ||
|
||
This could also be allowed in actual enum declarations, maintaining the analogy. | ||
|
||
```csharp | ||
enum TrafficLight | ||
{ | ||
Green, | ||
Yellow, | ||
Red, | ||
|
||
public bool Stop => this is not Green; | ||
} | ||
``` | ||
|
||
Additionally, if we continue to embrace the [Case declarations](https://github.com/dotnet/csharplang/blob/main/proposals/case-declarations.md) proposal, this could mitigate the cliff occurring when you want to add a member body to a case type: Simply put that case type as a case declaration: | ||
|
||
```csharp | ||
public closed record GateState | ||
{ | ||
Closed, Locked; // Simple cases | ||
case Open(float Percent) | ||
{ | ||
public static Open Fully => new Open(100); | ||
} | ||
} | ||
``` | ||
|
||
- Fully unify struct-like and enum-like declarations by allowing both a list of case type references and a list of enum-like union members in the same union declaration. This would be a superset of the proposal, but would go against the current decision to keep the two kinds of union declarations separate. It is unclear how many scenarios would benefit from this, but it is also unclear who would benefit from forbidding it. | ||
|
||
```csharp | ||
public record None; | ||
public union Option<T>(None) { Some(T value) } | ||
``` | ||
|
||
## Open questions | ||
|
||
- Does this coexist with [Case declarations](https://github.com/dotnet/csharplang/blob/main/proposals/case-declarations.md)? |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.