Description
Author background
- Would you consider yourself a novice, intermediate, or experienced Go programmer?
Experienced
- What other languages do you have experience with?
JavaScript, Pascal, Java, C, Scheme, Haskell, C#, C++, Python, PHP, Ruby
Related proposals
- Has this idea, or one like it, been proposed before?
Sort of. Nil for type variables, but not all types, has been proposed.
- If so, how does this proposal differ?
Nil for all types.
- Does this affect error handling?
No
- If so, how does this differ from previous error handling proposals?
N/A
- Is this about generics?
Yes/no.
- If so, how does this relate to the accepted design and other generics proposals?
It permits nil for type variable types, but also nil for other types.
Proposal
- What is the proposed change?
Currently, if I understand correctly, there's no expression for the zero value for a type variable:
type Map[K comparable, V any] struct {
ks []K
vs []V
}
func (m Map[K, V]) Get(k K) V {
for i, k2 := range m.ks {
if k2 == k {
return m.vs[i]
}
}
return zeroValue // cannot currently express this
}
This is a trivial example, but I've seen real questions about what to do in these situations.
Currently, if I understand correctly, the only way to do this is to declare a variable, and return that:
var zeroValue V
return zeroValue
Why not allow nil to be used as the zero value for type variables, to fill this gap?
return nil // == V(nil)
At runtime, nil would be the zero value for the specific type argument.
Nil could actually be interpreted as the zero value for every type, even outside of generics. The word "nil" means zero, anyway. This would be handy in situations where you make a type a pointer just to avoid lots of typing. For example:
if condition1 {
return ReallyLongStructName{}, fmt.Errorf(...)
}
if condition2 {
return ReallyLongStructName{}, fmt.Errorf(...)
}
Instead, you could keep the non-pointer type, and then do:
if condition1 {
return nil, fmt.Errorf(...)
}
if condition2 {
return nil, fmt.Errorf(...)
}
It would also solidify the type abstraction concept of generic functions, where functions varying only in concrete types can be abstracted into one generic function with type variables. For example:
// Same implementations, different types
func (m MapIntInt) Get(k int) int {
for i, k2 := range m.ks {
if k2 == k {
return m.vs[i]
}
}
return nil // nil, not 0, but means the same thing for int
}
func (m MapStringString) Get(k string) string {
for i, k2 := range m.ks {
if k2 == k {
return m.vs[i]
}
}
return nil // nil, not "", but means the same thing for string
}
// Same implementation, abstracted types
func (m Map[K, V]) Get(k K) V {
for i, k2 := range m.ks {
if k2 == k {
return m.vs[i]
}
}
return nil // nil is 0 for int and "" for string
}
Similar to how generics required an inversion of the meaning of interfaces to be type sets, perhaps generics also requires an inversion of the meaning of zero and nil values, where every type has a nil (default) value, and for number-like types, that just so happens to be the number zero, but for other types, it could be whatever makes sense for them.
- Who does this proposal help, and why?
Go users, because there would be a uniform way to specify the zero/default value for a type, even if that type is abstracted with type variables.
- Please describe as precisely as possible the change to the language.
Zero values would be re-defined as nil ("default") values. The nil values for numeric types would be zero(es).
- What would change in the language spec?
See just above
- Please also describe the change informally, as in a class teaching Go.
Nil is now assignable to any type, and now means the zero value for the type. This works for types that are either type variables or not. For type variable types, the specific value indicated by nil depends on the corresponding type argument.
- Is this change backward compatible?
Yes
- Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit.
Show example code before and after the change.
See above
- Before
var x T
ret = x
return LongStructName{}, err
- After
ret = nil
return nil, err
- Orthogonality: how does this change interact or overlap with existing features?
Similar to how generics required redefining the meaning of interfaces (type sets), generics should also require redefining the meaning of default values (from zero(es) to nil, which is zero(es) for numeric types).
- Is the goal of this change a performance improvement?
No
- If so, what quantifiable improvement should we expect?
N/A
- How would we measure it?
N/A
Costs
- Would this change make Go easier or harder to learn, and why?
Easier, because nil is already meaningful for about half the built-in types. This would add consistency to the language by making nil meaningful for all types, using a meaning ("zero value", "unassigned variable") already applicable to all types.
- What is the cost of this proposal? (Every language change has a cost).
Enabling nil to be assignable to all types, and making it equivalent to the zero value for each type.
Minor changes to the language spec to reflect that.
This would decrease the cost of understanding the language in terms of consistency for semantics.
It's a net win, in terms of cost. If you disagree, please answer these questions in your response:
-
According to what?
-
Relative to what?
-
How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
Only tools that type-check syntax. None of the tools listed here.
- What is the compile time cost?
Negligible
- What is the run time cost?
Negligible
- Can you describe a possible implementation?
Substitute nil
with the zero value for the type, e.g. 0
, ""
, T{}
, T(nil)
, etc.
- Do you have a prototype? (This is not required.)
No
All I ask
I didn't encounter counterarguments or alternatives in golang-nuts or Reddit that suggested a better or even viable alternative to the problem presented here. The problem identified is the lack of an identifier that means the zero value for a type. Please don't suggest, as an alternative, a non-identifier expression that gc
will currently evaluate efficiently to the same value, as that doesn't solve the problem identified. This proposal is about amending the Go language, not a Go implementation like gc
.
Before you respond, please consider whether the point you're responding to is the heart of the idea being proposed here. In my opinion, counterarguments should first seek to steelman their target.
A proposal is an argument. As such, this is a debate. Please participate in good faith, with rebuttable/refutable/falsifiable counterarguments, and by acknowledging (something like "I agree" or "I disagree because...") all the points made by the people you're responding to. Please don't "swoop" in with a counterargument, then "swoop" out immediately thereafter, and not stick around to defend it.