-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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: Go 2: universal zero value with type inference #35966
Comments
Or perhaps underscore as the zero designator. Either would be readable. |
Interesting idea. Sorry to bike shed, but perhaps the reusing the
|
I find the latter two more readable, but Here's another argument for the proposal. These calls highlight the values that are being passed, which is good:
This call highlights the type that is being passed, including its fully qualified name. This shifts the cognitive effort.
|
i think but as a concept of language proposal, i like your idea. |
This seems to be a restatement of #19642 with a different spelling of the zero value. Given that the earlier proposal was not accepted, what has changed since then? |
It's not stated why the previous proposal was closed, and I had not seen it when I searched and filed this proposal. I raised this proposal from direct and repeated experience. In addition to comments in this issue and the previous issues, I'll add another: There are up to three items of information in a Go expression or assignment: name, type, and value.
The function calls |
This proposal, and #19642, is something else again. It proposes a way of writing a value that can be converted to the zero value in a type context. Writing You could presumably write So I don't agree with your suggestion that there is some missing aspect to type inference. Untyped constants, |
Per your comment, untyped constants do support type inference, and overloaded nil does support type inference (issue with nil interfaces noted). The net effect of this is that type inference is neither uniform nor universal across data types. This is the impetus for my proposal and the earlier proposals. I also like #12854, and would consider any of these a positive step. |
I think we must mean different things by "type inference". I tried to describe exactly how untyped constants and |
By type inference, I mean the omission of the type name in the text of the value. Your example of
Would be useful to write
where type of v is SomeLongStruct. This proposal says that {} is treated uniformly as the zero value in all contexts where type can be inferred / determined. That seems uniform and universal. The concept of "zero value" is already universal, i.e. defined for all types. |
OK, omitting the type in the text of the value is what I would call an implicit conversion. Untyped constants support an implicit conversion to a set of related types, and also support an implicit conversion to a default type. The value Another case where implicit conversion occurs in Go is that any type that implements an interface type may be implicitly converted to that interface type. |
A better way to do this (in my opinion) would be to allow for constant struct expressions, which would hopefully include "untyped struct literals". #21130 gets close to this but isn't very specific, I might try to type up something a little more formal. |
Const-ness is orthogonal to type inference. |
Untyped constants are not, however. What I am proposing is that we should be able to do |
I think #12854 and #21182 would fill most of the gaps where this hurts in most code. Comparing a struct to its zero would still be a little awkward with this proposal or #12854 since you'd need to write Generating code or, in the future, writing generic code that uses zero values is still going to be awkward, as you don't know which form the zero value takes, though #21182 would knock out the most painful case. You can always do In most cases, you could probably get away with generalizing and having the user pass in a value, zero or not: for example, writing In generic code, comparing to zero also has a little wrinkle in that some incomparable types have a special case for comparing against zero that can't be matched in type constraints where you can only specify comparable or not. If there were some universal zero value, then #26842 could be accepted since there would always be a way to write a statically guaranteed to be all-bytes zero. But, if that's the only major case left and it would still be awkward to see if comparable structs are zero, maybe it would suffice to have a predeclared |
Yes, it's possible to do less. But I haven't seen any argument for why less is more in this case, or any downside to the universal zero. |
Let's consider what we can do with a specific, typed zero value,
If we had a universal zero value, then defining a new variable and calling a method are out, as a specific type is required for each. Using it as an operand isn't really a problem since any type with operators already has a concise zero value. That leaves:
For the majority of these, there's only really a problem if For comparable
but we couldn't write
due to the ambiguity and we would instead have to write
All of this assumed that we knew upfront what The most common case would be returning some zero values and an error. #21182 would allow that and also improve the readability and editability of non-generated/generic code as a bonus. That leaves us with a different set of possible problems:
A universal zero value would be useful here, but I think the majority of these will be relatively uncommon, though I could be wrong. A good way to make a case for this proposal would be to write reasonable generic code using the latest generics draft that is very awkward without a universal zero. Finding code generators that have a lot of special cases or past/known bugs because of this would be another. The one that seems like it would be most likely to cause problems is the split between incomparable types that are totally incomparable versus those that can be compared against |
As a detail, I don't see any ambiguity with
Every binary operator requires expressions on both sides, not statement blocks. |
That's true. I was thinking about how you have to write |
type T = func()
func Default(a, b T) T {
var zero T
if a != zero {
return a
}
return b
} This code doesn't compile because This doesn't matter much now, but in a world with generics, not being able to write |
@carlmjohnson there's also #26842. Consider |
Currently the language permits writing a simple expression, without specifying a type, for the zero value of most types: The raises the possibility of, rather than inventing a generic zero value, extending The idea here is that we could assign |
I have the impression that to you, Go is similar to Java. In Go, variables whose types have value semantics are never in an uninitialized state. That includes strings. Note that a reference type is a computer science notion (that used to exist in Go's documentation). It can't mean anything. Anyway, I could go on but the point is a bit moot if you had the impression that currently, every variable can be uninitialized. Hope it helps. |
FWIW I'm running into the issue @jimmyfrasche mentioned above, namely that I have a generic type and want to compare it to its zero value: type Sparse[T any] struct {
m map[Pos]T
}
func (g *Sparse[T]) Set(p Pos, v T) T {
// don't store zeros, that just wastes space
if v == *new(T) { // compiler error: T is not comparable.
delete(g.m[p])
} else {
g.m[p] = v
}
} Personally, I didn't think of this before (and didn't see it when it was brought up) and it causes me to re-evaluate my support for a proposal like this. Though I also thought about the predeclared |
I strongly support adding |
I have no strong opinions about the spelling of the zero value or whether we add a predeclared identifier for the zero value or add an Though I do oppose spelling the zero value And I think any spelling of a universal zero value will likely make it slightly worse. Because I think IMO, the ship on that has sailed and if anything, we should try and figure out how to clarify that you never want to compare the dynamic value of an interface without knowing its type. |
Accepting this duplication would also be confusing (perhaps even more). It would only be a convention to use 1 over another (something for style guides and linters to opine about 😬 ). Historically, Go has avoided introducing multiple nearly identical concepts. Properly understanding that I find extending [I was originally in favour of something short like |
To clarify, are you proposing making
I agree that this is my main reservation about adding |
var v struct{ _ [0]func() }
fmt.Println(reflect.ValueOf(&v).Elem().IsZero()) // true
fmt.Println(v == v) // does not compile
fmt.Println(v == nil) // does not compile
var zero struct{ _ [0]func() }
fmt.Println(v == zero) // does not compile It is annoying to me that you can test if v is the zero value, but only if you use reflect, and reflect is quite slow (50x slower than an elementary operation in some quickie benchmarks I've done, and also it can force allocations). |
@carlmjohnson I believe that if we introduced |
I'd rank the options:
TBH, I'm a little less confident at the moment with the order for 2/3. ("Is the extra complexity worth the benefit?"). However, I would really like a good solution -- |
It requires very little understanding of the Go spec to know I understand there is a desire to condense 2 lines of code into 1; as one of the lines feels unnecessarily verbose; but besides decreasing the amount of typing, what other benefits does a universal zero value provide in any form; ( i.e. For the sake of argument, let's say we do get a universal zero value (substitute your favorite How has Go improved if goes from: var zero T
if v == zero {
// something if zero
}
// ...
var zero T
return zero To if v == zero {
// something if zero
}
// ...
return zero I'll go first. func ManyTypes[A any, B any, C any, D any, E any](a A, b B, c C, d D, e E) {
var (
zeroA A
zeroB B
zeroC C
zeroD D
zeroE E
)
if a == zeroA { /*...*/ }
if b == zeroB { /*...*/ }
if c == zeroC { /*...*/ }
if d == zeroD { /*...*/ }
if e == zeroE { /*...*/ }
} I wonder how common of a case would something like that be? I've never need more than 2 parameterized types. Note: For In this case I'd probably be more inclined to support a new built-in |
We can allow any type to be comparable against that universal zero identifier, which means you don't need to constrain generic code on FWIW that's probably also a reason to make the universal zero |
In benchmarks for my truthy package, I've seen that |
@carlmjohnson that can't be right. Both generic and non-generic versions should inline. This benchmark shows no difference in the ns/op. |
@DmitriyMV on my machine, here's what I see:
|
I was able to reproduce @carlmjohnson 's performance difference. It has nothing to do with generics per se, just an optimization that sometimes failed to apply. See #59684. CL for a fix is in review. |
Thanks for the discussion here. I filed #61372 to try to move a concrete plan forward. |
The {} notation is likely ambiguous, and the alternative |
Closing as no further comments. |
I propose a universal zero value with type inference. Currently nil is a zero value with type inference for pointers and built-in reference types. I propose extending this to structs and atomic types, as follows:
{}
would represent a zero value when the type can be inferred, e.g. in assignments and function call sites. If I have a function:func Foo(param SomeLongStructName)
and I wish to invoke Foo with a zero value, I currently have to write:
Foo(SomeLongStructName{})
With this proposal, I could alternatively write:
Foo({})
For assignments currently (not initializations; post-initialization updates):
myvar = SomeLongStructName{}
With this proposal:
myvar = {}
This proposal is analogous to how nil is used for pointers and reference types.
The syntax allows type names and variable types to be modified without inducing extraneous code changes. The syntax also conveys the intent "zero-value" or "default" or "reset", as opposed to the actual contents of the zero value. Thus the intent is more readable.
The text was updated successfully, but these errors were encountered: