-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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
proposal: spec: exhaustive switching for enum type-safety #36387
Comments
Personally, I deal with this by writing code in a conservative way, like:
Then, exhaustive tests and code coverage will catch if I missed any of the cases. Certainly not perfect, but I've found that this works well. In multiple years of working with iota constants in Go, I don't recall a single time where I've had bugs that could have been avoided via exhasutive switches. |
If I understand this correctly, the suggestion here is to add two new language constructs. I'm going to try to restate them in words rather than entirely in examples. First, switch x.(const) Here It's not immediately clear what should happen if the type is exported and other packages define constants of that type. Do we only check for constants defined by the package that defines the type? Should we also check for constants defined in the package where the The second new construct is a new kind of type assertion x, ok := x.(const) As with the new switch statement, the language looks for defined The initial result being simply |
I note that one common request for an enum type is a way to iterate through the valid values. That is not supported here. Another common request for enum types is some sort of requirement that variable of the enum type can only take on valid values. That is only partially supported here, in that code can use the new type assertion syntax to verify that it has a valid value. I do like the minimalistic approach to ensuring that switch statements are complete, as that is probably the most common request for enum values. |
@ianlancetaylor Ian, thank you for jumping in to help me clarify. I don't have enough experience in language development and really appreciate the guardrails.
It was an oversight on my part not to slim it down. But the core goal was to show something that feels neat and quirky yet clear like Go is.
One thing I was thinking about is that if
There is another case I've thought of since I've reposted this as a separate issue: package inner
type State int
const (
A State = iota
B
C
) does not prevent, within or outside the package: const duplicate State = 1 In such cases,
In accordance with long-standing conversation about the lack of mapping functions, one would write the
I guess I would say that core purpose of this proposal is to introduce an incremental change which enables Would love to talk more to improve this. I added links to prior conversations I was reading through recently at the top. |
This comment has been minimized.
This comment has been minimized.
It's not clearly specified how to know which set of values are permitted for a type used with |
As this is a language change proposal, could you please fill out the template at https://go.googlesource.com/proposal/+/bd3ac287ccbebb2d12a386f1f1447876dd74b54d/go2-language-changes.md . When you are done, please reply to the issue with Thanks! |
@gopherbot please remove label WaitingForInfo |
@ianlancetaylor thank you for welcoming these thoughts and guiding me through the process. The template requirement allowed me to introduce additional thoughts on how this would work, but I am hoping there is room for further discussion before a final decision is made. Please review the template and let me know if it can be improved. As I had to make the leap and use Lastly, again on the subject of time and proposal review schedule. I will make my best effort to stay with this and be responsive, but hope for some decent lag to be accepted without pulling the plug. Sometime life gets in the way. |
The comment at #36387 (comment) needs to be addressed: how exactly do we know which constant values are associated such that the Since we can use this for |
I've addressed the overall concern in the code example, perhaps it was too brief and sneaky. Here are some more detailed thoughts:
In the proposal template code example I've highlighted the following: var method string = "GET"
method, ok := method.(const) // ERR: unable to check value of unnamed type against known constants which disallows a plain string to be matched against all possible string constants. The user either must use a named type (which has a known, fixed set of values to check against) or the |
If we aggregate valid constant values across packages, then the behavior of |
That's a good point. I didn't see the potential implications of that, although that's the allowance currently present with Do you think a requirement of a single block declaration is viable as an alternative extreme to the "everywhere" approach? I am uncertain. |
I'm not sure what the best answer is, but I'm fairly sure that it shouldn't vary depending on which packages have been imported. |
I agree. Adding an import shouldn't break or implicitly change behavior of existing code. |
We could say that a given constant type is internal to the package that defines it, adopting the behavior of protecting the func (s *pkg.Import) String() string // ERR Such approach would limit the There just isn't (yet) a way to distinguish a type: type Method string as a "constant" type... It makes sense though — the types defined using that syntax for the purpose of being a constant enumeration are rarely used as a general purpose variable. The only case is the type casting, where we say At the same time, a definition of a "constant type" would eliminate The Except the famous |
No. |
Constants declared in the same package as their defined type are often enums—but not always. When they are, what an individual package means by enum is not at all uniform. I'm not sure if what is meant by enum here is the best definition, but I do like that it tries to piggyback on ordinary consts as much as possible. Maybe if there were a special way to more tightly associate consts with their type, like say const type T int (
A = iota
B
C
synonym = C
)
const regular = B All the constants in that block are implicitly type
Otherwise, it's the same as type T int
const (
A T = iota
B
C
synonym = C
)
const regular = B This association could just mean that those (label, value) pairs are stored in the export data with That would allow a lot of linters and libraries for handling various enum-ish things to be created to handle various enum semantics. You can kind of do that now for linters but it gets complicated since you don't know which consts are associated with the type, per the above definition. Runtime checking requires the associated labels and values to be added to the program either manually or with code generation. The notion of associated label/values could also allow some special syntax like |
@jimmyfrasche thanks for chiming in. I will try to comment and clarify, what you and @ianlancetaylor shared is very helpful. The reason I had idea for "piggybacking" on the ordinary It seems like a lot of issues have been created by the ambiguity of the syntax I proposed, which refers to the constant scope in question using I now see that my affection for the type LetterIndex const int
//---------------^^^^^---- restricts this type to declarations of integer constants
const (
LetterIndexA LetterIndex = iota+1
LetterIndexB
LetterIndexC
) Further declarations of typed constants within package scope amend the set of known values: const LetterIndexZ LetterIndex = 26 // valid Declarations that duplicate values defined earlier in code are synonyms and simply point to the address of the unique value in memory: const (
Last = LetterIndexZ
First LetterIndex = 0
) The precedence and unique requirement encourage "single block" declaration, without restricting odd uses. Declaration of a var cast = LetterIndex(1) // ERR: cannot convert 1 (untyped int constant) to type LetterIndex But we introduce two new ways of retrieving a value of a named type: // Type assertion
var v = 1
cast, ok := v.(LetterIndex) // cast = LetterIndexA, ok = true
// Exhaustive switch
switch v.(LetterIndex) {
case A, B, C:
break
default: // required, "25" was not handled
break
} Finally, the compiler is using a known set (list of unique values) to accommodate these new features. It is reasonable to expose this set to the users as well: fmt.Printf("%+v", values(LetterIndex))
// [LetterIndexA, LetterIndexB, LetterIndexC, LetterIndexZ] You can see that I replaced the use of |
Regarding the absence of a proper enum type in Go, a quick observation: the source code sequence
Can be viewed as a very verbose way of writing
but with the advantage that the underlying representation size of the enum is stated explicitly. I was initially disconcerted by this surface syntax, but it's a modestly clever bit of sparsity in the language design. |
One of the patterns that is occasionally used in enumerations looks like:
You can see this pattern used in the go In this pattern, you wouldn't usually expect There are variants on this pattern where How does your proposal interact with this existing practice? |
Would you consider yourself a novice, intermediate, or experienced Go programmer?
Intermediate, multiple projects, plenty of learning
What other languages do you have experience with?
JS/TS, Swift, Python, C#, C++
Has this idea, or one like it, been proposed before?
Yes, the enums are a popular under proposal subject matter.
If so, how does this proposal differ?
It doubles down on the status quo (aka historically idiomatic) solution for enums in Go. All current Go enum declarations will remain valid. Instead of introducing new keywords or interfering with existing patterns (such as the Stringer interface) it adds tools for enforcement and runtime-safety of enumeration usage. It does so by employing existing type assertion and type casting idioms, and the
const
keyword idiomatic to Go enum declarations.Who does this proposal help, and why?
For those adopting enums in Go, it is important to limit their use to the values known at compile time, or explicitly opt-in to the type-casting and therefore verifying the primitive values. The new syntax will not eliminate the need for authors of packages exposing an enumeration type to create checks on enum usage. Rather, this proposal will clarify the party responsible for enum value validation and provide tools that will make that validation idiomatic, clearly identifiable and easy to implement.
Is this change backward compatible?
Yes.
Show example code before and after the change.
I used http package as an example but I do not advocate redeclaring standard library constants as enums.
Before
After (please see comment below for an evolved, but still work in progress update to the proposed changes)
What is the cost of this proposal? (Every language change has a cost).
The new syntax clashes with
.(type)
and theconst
addition to function signature argument is opening a can of worms.How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
New syntax would depend on linting and formatting to ensure ease of adoption.
What is the compile time cost?
Compiler would need to identify all references to a named
const
type and ensure the binary has type-specific facilities for executing.(const)
checks. Additionally, when a function signature (or perhaps even a struct field) requiresconst
argument, it must identify the call site and ensure the.(const)
conversion takes place prior to call.What is the run time cost?
During program execution the new
.(const)
checks are done against switch cases or statically instanced collections of pre-defined values. If the type casting for enum values is present in the binary, and the new syntax is employed unsafely (e.g. the.(const)
casting omitsok
variable), thepanic
messaging must clearly identify the mismatch between available pre-defined enum constants and the result of the runtime-cast of from a primitive type.Can you describe a possible implementation?
No. I don't have a familiarity with the Go source code. Point me in the right direction and I would love to contribute.
Do you have a prototype? (This is not required.)
No, but willing to collaborate.
How would the language spec change?
I don't have an off-hand grasp of EBNF notation, but in layman's terms the spec will extend type assertion, type switch, and function parameter usage to make possible the new syntax.
Orthogonality: how does this change interact or overlap with existing features?
It makes use of type assertion and type casting syntax as the basis.
Is the goal of this change a performance improvement?
No.
Does this affect error handling?
Yes, the advancement of enum support in Go through this or another proposal must ensure more elaborate analysis at exception site. In case of this proposal, it is now possible to identify
If so, how does this differ from previous error handling proposals?
I don't know.
Is this about generics?
No.
Original ideas and research
Prior Art
In no specific order:
case
keyword has already been appropriated for enum declarations in proposal: spec: enum type (revisited) #28438 leading to defacto parity with language like Swift.gofmt
status quo.iota
usage extension has been mentioned in proposal: permit iota, omission of init expressions in var declarations #21473 and I take it as a signiota
will not be going away...Thoughts
enum
is indeed new type, which is whattype State string
does, there is no idiomatic need to introduce a new keyword. Go isn't about saving space in your source code, it is about readability, clarity of purpose.Lack of type safety, confusing the new
string
- orint
-based types for actual strings/ints is the key hurdle. All enum clauses are declared asconst
, which creates a set of known values compiler can check against.Stringer
interface is the idiom for representing any type as human-readable text. Without customization,type ContextKey string
enums this is the string value, and foriota
-generated enums it's the integer, much like XHR ReadyState codes (0 - unsent, 4 - done) in JavaScript.Rather, the problem lies with the fallibility of custom
func (k ContextKey) String() string
implementation, which is usually done using a switch that must contain every known enum clause constant.In a language like Swift, there is a notion of an exhaustive switch. This is a good approach for both the type checking against a set of
const
s and building an idiomatic way to invoke that check. TheString()
function, being a common necessity, is a great case for implementation.Proposal
P.S. Associated values in Swift enums are one of my favorite gimmicks. In Go there is no place for them. If you want to have a value next to your enum data — use a strongly typed
struct
wrapping the two.Originally posted by @ermik in #19814 (comment)
The text was updated successfully, but these errors were encountered: