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 |
For basic types Go already has special syntax for their values: We can write numbers (incl. Thus, at least with existing Go I don't see a good reason for introducing an alternative way of writing those zero values differently. That may change when we have generics where we may want to introduce a zero value that can be written in a type-independent way. But I do like @ianlancetaylor's idea of generalizing This should be a backward-compatible change. In generic code we might go the extra step and permit |
Generated code exists today, will exist after generics, and can get annoying when zeroes are involved. You either need to write it in an unnatural manner or figure out which zero value to use based on type analysis, but at least the latter would be reduced to selecting from { Still, just allowing |
Extending |
The fact that these all vary depending on the underlying type suggests to me that structs and arrays deserve their own different zero expression instead of overloading the meaning of |
Is this a good analogy? In those cases, functions like |
One thing to consider is the ability to take the address of a zero value:
|
The proposal lists the two problems below as the problems it solves in the issue description, but neither appear to me as compelling problems worth solving and I can't find conversation here where the problems are considered in isolation of a proposal. Are these problems worth solving? Are there other problems that this solves that I'm missing? To focus on each problem: 1 –
Modifying code is something code is great at, which is why we have IDEs and tools like gopls. In the examples in this issue where the type name has been removed I find the code ambiguous. I find it very helpful that I can look at a line of code setting a variable with a struct value and see exactly what struct value is being set. I'm struggling to imagine a situation where using 2 –
I don't think |
We already have |
You're right, my mistake! Can we learn something from my mistake? (Hopefully I haven't lost all credibility!) Slices are certainly more opaque than plain structs. As a regular Go user not working on internals, I didn't fully appreciate the fact that |
@ianlancetaylor wrote:
The other place where this would be useful, and probably most useful in my opinion, is in return values. It is common to have a function that returns
We often work around this using named return values, but that is fraught with concerns about variable shadowing. The proposal to use nil as the zero value for structs would yield this instead:
which is undoubtedly less verbose and annoying than typing the struct literal. -- As convenient as this seems, I don't much like it. My rationale: We use Using I would prefer that rather than overloading the meaning of |
Firstly, I see no real need for having a universal zero value for non-generic code so I am not in favor of this proposal as it stands. However, I do think it would be a good idea if there were a 'short' default value for structs and arrays which could be used instead of the present verbose syntax, though I agree with @adg that (whilst convenient in some ways) I think it is too embedded in people's thinking that Also (to me at least) it would seem anomalous if So I think we need something different and, to derive some benefit from the present proposal, why not just use As far as generics are concerned, I think we really do need a universal zero value for cases where a unique zero value cannot be deduced from the contract (if any). Although it would work, Again, I think it would be best if something were used which was unique to generics and my personal favorite would be to overload the |
Using I don't see the point of adding a zero that's not universal. Generics will need a universal zero, so you can write:
You can't write that without a universal zero, and adding nil as struct zero doesn't really help, since it won't work for numbers and strings. |
I don't know the issue number (edit: #22729 in the discussion above), but this is going in the opposite direction of the proposal to make nil interface less confusing by renaming it to something else ( I guess the only way it would be a good idea would be to go the whole way, use |
I have noticed that marshaling as JSON a nil slice results in |
@carlmjohnson yes I understand that, but I've had a clear mental model of the distinction between a nil slice and an empty one. If |
I'm generally in support of this proposal, I think. However, I speculate that this may increase confusion with |
@eandre I don't think any proposal changes anything with regard to the distinction between |
You can just define a new variable which will default to the zero value of that type and use it for comparison as done here https://go-review.googlesource.com/c/go/+/187317/13/src/cmd/go2go/testdata/go2path/src/slices/slices.go2#81
However having a universal untyped zero value may be clearer. |
@nemith, that doesn’t work for func(), which can only be compared to a constant nil and can’t be declared as const. |
@carlmjohnson Also slice and map types. |
Yes, #26842 is very relevant. Some otherwise incomparable types are zero-comparable:
|
#22729 add kind-specific nil predeclared identifier constants Here is my attempt at a summary of the problems and possible solution paths. Problems:
Possible solutions:
|
A similar idea as adding new keywords, but to stay backwards compatible, would be a new a new built in function zero(), which could be added, and which returns the "unversal zero" for the variable it is assigned to. Buitin functions are the most go-ish approach to this problem. |
@beoran If it doesn't take any parameters and doesn't have any side effects, no point in making it a function. It can just be a value, like |
I mention above that a magic function would help with generic programming but not typing out return values or nil interface/pointer confusion. |
As @ianlancetaylor pointed out, Using Two more ideas that I did not see yet, which leave out the 'type inference' part, are:
|
|
You can’t compare func/map/slices to it though, which is a limitation compared to a predeclared constant. |
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: