Skip to content
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

spec: add untyped builtin zero #61372

Closed
rsc opened this issue Jul 15, 2023 · 355 comments
Closed

spec: add untyped builtin zero #61372

rsc opened this issue Jul 15, 2023 · 355 comments
Labels
Milestone

Comments

@rsc
Copy link
Contributor

rsc commented Jul 15, 2023

I propose to add a new predeclared identifier zero that is an untyped zero value. While nil is an untyped zero value restricted to chan/func/interface/map/slice/pointer types, zero would be an untyped zero value with no such restrictions.

The specific rules for zero mimic nil with fewer restrictions:

  • zero is assignable to any variable of any type T that does not already have a short zero (0, "", nil), including when T is a type parameter with constraint any.
  • A value of any type T may be compared to zero when it cannot already be compared to a short zero (0, "", nil), again including when T is a type parameter with constraint any.

That's it. That's all the rules.

Note that assignability includes function arguments and return values: f(zero) and return zero, err are valid.

See CL 509995 for exact spec changes.


This proposal addresses at least three important needs:

  1. Referring to a zero value in generic code. Today people suggest *new(T), which I find embarrasingly clunky to explain to new users. This comes up fairly often, and we need something cleaner.

  2. Comparing to a zero value in generic code, even for non-comparable type parameters. This comes up less often, but it did just come up in cmp.Or (cmp: add Or #60204).

  3. Shortening error returns: return zero, err is nicer than return time.Time{}, err.

More generally, the zero value is an important concept in Go that some types currently have no name for. Now they would: zero.

Because zero is not valid anywhere 0, "", or nil are valid, there will be no confusion about which to use.


I'm not claiming any originality in this proposal. Others have certainly suggested variants in the past, in quite long discussions. I'm not aware of any precise statement of the exact rules above, but I won't be surprised if one exists.

A brief comparison with earlier proposals:

@rsc rsc added the Proposal label Jul 15, 2023
@gopherbot gopherbot added this to the Proposal milestone Jul 15, 2023
@gopherbot
Copy link

Change https://go.dev/cl/509995 mentions this issue: builtin, spec: add builtin untyped zero

@mvdan
Copy link
Member

mvdan commented Jul 15, 2023

Assuming that you prefer zero over _ in terms of the syntax for a zero value or predeclared identifier, could you expand on your reasoning?

I personally find zero to be clearer and consistent with nil, although I admit that return _, _, err is shorter and feels nicer than return zero, zero, err. It also mirrors _, _, err := foo(), for example.

@mvdan
Copy link
Member

mvdan commented Jul 15, 2023

As far as idiomatic discussion and naming, I expect that zero will only be used for these general uses and will not displace nil as a more specific kind of zero value. In particular, we will keep using terms like nil pointer and nil interface; we will not switch to saying zero pointer, zero interface, and so on.

I would really like for the spec and builtin change to include guidance on this. That is, I assume we want "idiomatic Go" to not replace most uses of nil with zero, except perhaps where it helps with consistency, like rewriting return nil, time.Time{}, err into return zero, zero, err.

We really want to discourage rewriting if err == nil into if err == zero, for example. That sort of change would be noisy and make Go code less consistent across codebases, unless everyone does the big rewrite - which seems unlikely.

@josharian
Copy link
Contributor

josharian commented Jul 15, 2023

An alternative is to adding zero is removing the restrictions for nil. Can you share some about the thinking to prefer adding zero?

@josharian
Copy link
Contributor

except perhaps where it helps with consistency, like rewriting return nil, time.Time{}, err into return zero, zero, err

FWIW, I would still prefer #21182 here (return ..., err). I think that it is my single favorite open proposal. (Although I'm also partial to 128 bit ints. :P)

@mrwormhole
Copy link

mrwormhole commented Jul 15, 2023

I am not a super smart guy but, I do think explaining *new(T) = *&struct{} is useful and not awkward at all, I come from strong C background, I think everyone who is new should learn what "*" or "&" stands for avoiding incorrect usage, some codebases actually pass **type or ***type without realizing because of this lack of knowledge.

Secondly, I think standard cmp package shouldn't be reasoning behind any change because we as developers now going to bump into same package names with 1.21 (because we use google/go-cmp primarly for our testing and IDEs will show up 2 results now, did you mean this or that etc), I am personally not happy with 1.21's cmp package direction

Thirdly, I think explaining zero as a concept over struct{} will be harder for everyone and in most cases, we never return allocated struct and error at the same time, it should be the dev who is assigning to a default when error occurs, not the other way around. I always never liked returning struct{}, err or nil,nil . Zero will hide the allocation detail for concrete types, I think allocation must be obvious to the reader's eye (new is the exception here)

apologies for the noise from me

@earthboundkid
Copy link
Contributor

I prefer zero to _ because it would be weird to say if f == _.

@Merovius
Copy link
Contributor

Merovius commented Jul 15, 2023

FWIW I don't have strong opinions about how to spell a universal zero value - I'm fine with any color for that bikeshed.
I do think this proposal (or this proposal with s/zero/_/g) has the advantage of addressing several semi-related issues with a single, easily understood mechanism.

@mrwormhole

Secondly, I think standard cmp package shouldn't be reasoning behind any change […]

The justification isn't the cmp package, it's that we need a mechanism to compare values against their zero value, even if their constraint is not comparable. All types allow doing that, but there currently is no way to write a generic function that does it.

The cmp package is one consumer of such mechanism, but not the only one. Personally, I ran into this with a container type wrapping map, where I would have preferred not to store zero-values, as they take up memory without carrying any semantic benefit for my use case, where "not stored" and "the zero value" where semantically equivalent.

I think it's fair to criticize arguments like the explainability or readability of *new(T) for their subjectivity. But this particular problem has no solution without a language change. And "I don't like the package name cmp" is obviously not a very compelling reason not to add such a mechanism.

@jimmyfrasche
Copy link
Member

this proposal would also satisfy #26842.

@rsc would accepting this proposal also involve changing cmp.Or to accept any type and use == zero or would that need to be a separate proposal?

@josharian #21182 could be additionally accepted. It would be less needed than it is now but there's still an argument to its utility and if it were accepted instead of this proposal there'd still be a need for the additional functionality contained in this proposal.

@seebs
Copy link
Contributor

seebs commented Jul 15, 2023

I like this a lot. I also like _ for the universal-zero.

What I almost want is for zero to be a valid value for every non-pointer value, but not for pointers, which need nil. I say "almost" because in some contexts, especially generics, I don't know whether a value happens to be pointer-ish.

What I really want is "zero is a universal zero that you can use except when you know you are thinking about a thing in pointer terms, in which case you want nil". Although thinking about it more, at least two cases where I currently use nil (slices, maps), I think "zero" would be comparably/similarly expressive.

I am now very conflicted on whether I think it's more consistent to call the zero value for a slice zero or nil in such a case. I definitely prefer nil for pointers, though.

@seebs
Copy link
Contributor

seebs commented Jul 15, 2023

Okay but thinking about it more, I have concluded:

I would also be fine with just extending nil to all types, including non-pointer types. I'm not actually going to be particularly confused by seeing return nil, err in a function returning a non-pointer type for any length of time, we already have the word, and it's good at expressing "i don't actually want/need a value here".

Observation: That you can use nil for slices, maps, and interface values, and in each case it means a thing that is more complex than a simple "nil pointer" is sort of an argument that we already effectively do this. We have at least three things which are, internally, actually structs of some kind, for which nil is a valid value.

@AlexanderYastrebov
Copy link
Contributor

Could it be just 0 instead of an identifier? (return time.Time{}, err -> return 0, err)

@earthboundkid
Copy link
Contributor

@seebs I think it would be interesting to allow null for pointers and no other types. But I don’t think that addresses a pressing problem in the same way. It’s more of a “if I wrote Go 2” idea.

@zephyrtronium
Copy link
Contributor

A linter to complain about zero where nil could be used instead seems like it would help preserve existing idioms.

@earthboundkid
Copy link
Contributor

would accepting this proposal also involve changing cmp.Or to accept any type and use == zero or would that need to be a separate proposal?

cmp.Or hasn’t been merged yet (it’s on hold until 1.21 is released), so I think it could just change to T any without a discussion.

@AndrewHarrisSPU
Copy link

Could it be just 0 instead of an identifier? (return time.Time{}, err -> return 0, err)

It might be funny business if 1-1 and 0 behaved differently :)

@willfaught
Copy link
Contributor

Why won't case zero work?

What guidance do you propose to give for when 0 vs. zero and nil vs. zero should be used? In other words, what would be idiomatic? For example, for func F() (int, string, time.Time, error), should it be return 0, "", zero, nil or return zero, zero, zero, zero?

I'm concerned by the direction the language is evolving. I see non-orthogonal features like this being added instead of existing features being generalized. There is already a zero value in Go: nil. If you lump all the built-in number types together, most built-in types have a nil value. Numbers, strings, arrays, and structs are the exception. Instead of adding something new to solve this one problem, let's generalize what we already have: make nil work for all built-in types. This has been proposed many times by many people. I'm disappointed that this proposal didn't address why this obvious solution won't work. My vote is no until it's changed to do so.

FWIW I don't have strong opinions about how to spell a universal zero value - I'm fine with any color for that bikeshed.

As an aside, I don't agree that coming up with a good name is bikeshedding. Naming is often characterized as one of the two hard things in computer science. Whether or not you agree with that quotation, naming is indeed important, and entirely relevant to the quality of a software design, as opposed to a nuclear power plant design committee getting derailed by the paint color for a bike shed.

@jimmyfrasche
Copy link
Member

I would write return 0, "", zero, nil unless it became idiomatic to do something else

@Merovius
Copy link
Contributor

@willfaught

Why won't case zero work?

Currently, it works because case nil is special cased in a type-switch. If we'd want case zero to work, we'd have to similarly special case it (note that in a type switch, the regular "check for the zero value" logic doesn't work, because the cases are types, not values). There seems to be no compelling reason to do so, especially if we generally advice to use nil, if it works.

@AndrewHarrisSPU
Copy link

I feel like zero is a great name because it results in a bit pattern of just zeros following assignment, and zero'd bit patterns are already so fundamentally baked into the language. Occasionally it is just easier to think about the bits.

@soypat
Copy link

soypat commented Jul 15, 2023

Some have favored removing the restrictions on nil instead of bringing in the zero to Go. I'd be curious on hearing arguments against it. So far comments in here and on the slack performance channel on it have all evolved from uncomfortable feeling to quiet acceptance.

So often have I refactored return myType{}, err to return nil, err when really there was no semantic difference in my program.

@Merovius
Copy link
Contributor

@willfaught

As an aside, I don't agree that coming up with a good name is bikeshedding. […]

FWIW there is more to the bikeshed analogy than just the importance of the question. But, in any case, I was merely trying to express that it doesn't matter to me. I want something to happen and I find iszero(T), zero, _, nil… all satisfying, personally. I would support any of them. I just strongly oppose doing none of them, because we can't agree on how to spell zero.

@seebs
Copy link
Contributor

seebs commented Jul 15, 2023

So, I think the messiest aspect of this is roughly the behavior in ambiguous contexts. If you're returning an interface value, then return concrete{} and return zero are not the same.

I could see a hypothetical benefit to a zero being distinct from nil if zero were restricted to needing a concrete type. So, it can be type-inferred, but if the inferred type is an interface, that's an error. Thus, you'd have to do something like return concrete(zero) to return an (interface wrapping) a zero-valued concrete type, and nil to return a nil interface.

Right now, if you are actually returning an interface type, and you have a lot of return &concrete{...} in the function, it's easy to miss that return nil is not the same thing as return (*concrete)(nil) in this context.

So I think zero could be worth it if it added that. Otherwise, just allow nil to be used as a generic zero even for not-at-all-pointery things, and everything's solved. No existing code breaks, no worries about namespace.

@josharian
Copy link
Contributor

it's easy to miss that return nil is not the same thing as return (*concrete)(nil) in this context.

Returning (*concrete)(nil) is, in my experience, almost always a bug. It even has an FAQ entry: https://go.dev/doc/faq#nil_error. So making it easier to do that doesn't seem like a priority to me.

@fzipp
Copy link
Contributor

fzipp commented Jul 15, 2023

The only thing that is not possible today is the zero value comparison, at least not without reflect.

To create a generic zero value var zero T; return zero and *new(T) are established patterns that work. Also, return time.Time{}, err has worked for 14 years.

That's why I'm in favor of an iszero() builtin function and no zero otherwise. It fixes the one thing that is not possible today and avoids the other questions.

@mitar
Copy link
Contributor

mitar commented Jul 15, 2023

There is confusion today between nil pointers and nil interfaces containing nil pointers.

I think if zero does not address this, then I see little point of having zero over simply relaxing nil. I in fact would prefer if err == zero to mean that I would like err to be a nil pointer of any interface, not just nil interface. But the proposal above does not propose that, so then why we just do not relax nil?

@atdiar
Copy link

atdiar commented Sep 15, 2023

@Merovius it is still unclear to me as zero stands for the zero value. zero itself is just the identifier.

Nowadays, at compile time, incomparable struct types comparison to their zero fail at compile time and panics at run-time in interfaces.

That behavior would change.
It's interface value comparisons where one interface value holds the zero that I am wondering about.

Again that might be far-fetched. I believe people started doing that with the introduction of generics.
Just a quick check.

@Merovius
Copy link
Contributor

@atdiar This has already been discussed. I'm not speculating - it works like nil works today. That analogy answers whatever questions you might have. If you understand the language today, then you'll understand the language with zero.

@atdiar
Copy link

atdiar commented Sep 15, 2023

@Merovius then it is a backward incompatible change in absolute and my question still stands.

Edit: I stand corrected on that. It's not a backward incompatible but then introduces some kind of inconsistency.

@merykitty
Copy link

@atdiar zero is zero, it does not stand for the zero value. zero being implicitly convertible to a value of type T does not mean it is a value of type T. And a value of type T being able to be compared with zero does not mean it is comparable with another value of type T. All of these behaviours are exactly the same as how nil works today.

@Merovius
Copy link
Contributor

@atdiar Please read the comment I linked to and the discussion leading up to it. Your questions have been answered. And no, it is simply untrue that there is any compatibility problem.

@atdiar
Copy link

atdiar commented Sep 15, 2023

@Merovius @merrykitty
I apologize. Re-checked and it appears that it's indeed exactly like nil.

I'm not sure I like that too much.
comparison to zero is unlike a comparison to an unassigned variable that holds a zero value or even a manually-defined zero value.

For nil, it wasn't an issue since there was only one way to write the zero value. For instance []int zero value can only be written nil.
For struct{[]int}, it'd be either zero or struct{[]int} {nil} but the behavior would be different if I am not mistaken?

https://go.dev/play/p/SA8FDVPZbwF

I find it a bit odd. But alas.

@Merovius
Copy link
Contributor

You are correct that that's how it would work. You are not quite correct that this isn't a thing with nil already.

@atdiar
Copy link

atdiar commented Sep 15, 2023

You're right. At the same time, even in the spec, the zero value is nil for this type. Not even typed nil although it probably is. No one really uses a type conversion.
But I appreciate the counter-example.

Still, in usage, we already have many zero values for structs and zero is actually not an identifier for them.
I think that should perhaps be made clear(-er?), that zero stands on its own.

But in that case, I'd really rather have a predicate or something else. It's a bit confusing.

@rsc
Copy link
Contributor Author

rsc commented Sep 22, 2023

This discussion has made clear that we're not ready for this change. Retracting the proposal.

@DmitriyMV
Copy link
Contributor

DmitriyMV commented Sep 22, 2023

@rsc wait, can you elaborate a bit?

You said in the last message that "No new information has been presented since the proposal was accepted.". And now it suddenly closed and retracted. What changed since then?

@earthboundkid
Copy link
Contributor

earthboundkid commented Sep 22, 2023

The new information was the sheer lingering of this thread.

I'm sad about the decision because I liked zero, but I can't fault the reasoning.

@DmitriyMV
Copy link
Contributor

DmitriyMV commented Sep 22, 2023

The new information was the sheer lingering of this thread.

If we apply the same reasoning to the generic proposal, we would never got generics. As well as min/max proposals which were controversial too.

This proposal had overwhelming support, few edge cases, it pas properly discussed and accepted. It is Russ right to retract it, because it's his proposal. But I'm going to admit that closing and retracting things like that, without any explanation, is demotivating for all who participated in discussion.

@ianlancetaylor
Copy link
Contributor

I don't think that the comparison with generics is quite right. There were several public proposals about generics that had a great deal of discussion and were then withdrawn and reconsidered. It's true that they weren't formally accepted, but the general idea was the same: we didn't move forward with generics until there was broad (though not universal) agreement.

And I wouldn't say that this proposal was withdrawn without explanation. The explanation was that many people still disagree with the proposal, as can be seen in the discussion on this issue. The acceptance may have been premature, or the acceptance may have led people to think further about it and led them to disagree.

@DmitriyMV
Copy link
Contributor

DmitriyMV commented Sep 22, 2023

@ianlancetaylor

The explanation was that many people still disagree with the proposal, as can be seen in the discussion on this issue.

With all due respect, that would not be the first accepted proposal where many people disagreed.

The acceptance may have been premature, or the acceptance may have led people to think further about it and led them to disagree.

And that contradicts @rsc notes about "Reconsideration". That also means that with sufficient repeating of the same points from the same people any, even accepted, proposal can be declined in the end.

What I'm trying to say, it's IMHO wrong to say "we are not going to reconsider our decision until new data arrives" and then reconsider it without any sort of explanation about what sort of new data arrived. That also kills any initiative to participate in any proposal discussion since rules of accepting/declining/reverting are arbitrary and can change at any point.

P.S. With this and #62487 both retracted, probably some Google internal discussion happened which lead to this? And if so, why not share the summarized details?

@ianlancetaylor
Copy link
Contributor

With all due respect, that would not be the first accepted proposal where many people disagreed.

The goal of the proposal process, as described at https://go.dev/s/proposal, is to reach consensus. If the proposal process is accepting ideas where many people disagree, then I hope that many more people agree, or at least that it is clear that the proposal is important for some other reason. Are there specific proposal that you have in mind?

and then reconsider it without any sort of explanation about what sort of new data arrived.

Respectfully, more than one person has explained what the new data was.

It's true that the proposal committee (which is not only Googlers) and others discussed this issue off line. That discussion amounted to: we have not reached a consensus here. And the evidence for that was the continued discussion on this issue.

I'm sorry that you feel that this kills any initiative to participate in any proposal discussion. I hope that most people don't feel that way.

@DeedleFake
Copy link

I'm sorry that you feel that this kills any initiative to participate in any proposal discussion. I hope that most people don't feel that way.

I think the main source of confusion was really just that @rsc announced its withdrawal kind of suddenly. If he had just had an extra few words in there, even something like

In light of new information that has been brought to light in recent comments, this proposal has is being retracted despite its accepted state.

it wouldn't have been as confusing.

@jimmyfrasche
Copy link
Member

@ianlancetaylor

It's true that the proposal committee (which is not only Googlers) and others discussed this issue off line. That discussion amounted to: we have not reached a consensus here. And the evidence for that was the continued discussion on this issue.

Then why close the discussion instead of just retracting the acceptance and saying where we're at and where we go from here? It seems like you're saying the approach in this thread is now off the table entirely.

@ianlancetaylor
Copy link
Contributor

Personally I don't think this approach is off the table. I do think that we need to step back and reconsider the problem.

We know from experience that proposal issues are not good places to discuss a problem. This issue already has over 300 comments. Keeping this issue open is not a useful path forward.

@DmitriyMV
Copy link
Contributor

DmitriyMV commented Sep 22, 2023

@ianlancetaylor

It's true that the proposal committee (which is not only Googlers) and others discussed this issue off line. That discussion amounted to: we have not reached a consensus here. And the evidence for that was the continued discussion on this issue.

And that what exactly is missing. Just like @DeedleFake said - a few phrases explaining, that, despite proposal being accepted, consensus was not reached neither here nor in committee internally and because there is no clear part forward, proposal is un-accepted and closed for a time being. Small explanation is all that needed. The reason for this need is also quite simple - Russ made a statement that merely disagreeing or continued discussion of the same points on accepted proposal is not enough to reverse it. But then suddenly it is.

Are there specific proposal that you have in mind?

#59488 (comment) comes to mind, where there was no agreement about min being builtin or part of cmp package during the discussion. In the end, the Go team decided to go with builtins and Russ explained this decision even tho there was people who would prefer to have them as the usual functions.

@changkun
Copy link
Member

changkun commented Oct 9, 2023

Someone made me notice that this proposal was retracted, but I left the impression that this was accepted. Apparently, there are tons of discussions that happened after the proposal was accepted, but it is not entirely clear, neither from this proposal (top message, or have to click through "load more") nor from #33502.

Considering this special case, @rsc, could you perhaps add the decision to #33502 as well so that people get to know that the status is retracted rather than accepted?

@perj
Copy link

perj commented Oct 12, 2023

Perhaps these kind of comments are not really wanted, but I wish to express my hope that this returns in some form or another, having to change the empty values in my return statements is one of my current annoyances with the language. #21182 does indeed look interesting.

@TheCoreMan
Copy link

I'm really appreciative of you @rsc, for suggesting something, getting it accepted, and having the humility to retract it. Good move.

@jhw0604
Copy link

jhw0604 commented Feb 7, 2024

I don't want the language to get any more complicated.
If typing is a hassle, one way is to borrow the power of an IDE.
If there is a feature that needs to be added, I hope it is a case that cannot be solved using existing methods.

@griesemer
Copy link
Contributor

@jhw0604 This issue is closed. We're not adding a built-in zero.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Declined
Development

No branches or pull requests