-
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: spec: permit using nil with type parameters #62487
Comments
@ianlancetaylor This sort of confusion is already permitted in Go: func F() func() *int
var _ = F() == nil
var _ = F()() == nil Is your argument that enabling that was a mistake, and we should minimize the number of ways this sort of mistake can happen in the future? Using nil in generic functions, and zero elsewhere, adds another non-orthogonal dimension to the language, increasing the language complexity between those dimensions geometrically. What is your argument for why that complexity is less than the complexity of using zero or nil everywhere? |
The proposed expansion of Overall I don't think this proposal is an improvement over #61372 in terms of complexity, explainability, and general language coherence, but it is substantially less useful (because it doesn't change error returns). |
@willfaught I agree that function calls can lead to confusion with
To be clear, I'm suggesting that we not add the predeclared identifier
People must already use |
Yes.
The more I think about #61372 the more reluctant I am to explain why there are four different ways of writing the zero value. |
Thanks. I missed that.
The point applies to any operation and nil-able type: var X chan map[int]int
var _ = X == nil
var _ = <-X == nil
var Y [][]int
var _ = Y == nil
var _ = Y[0] == nil These are just as "confusing" as You can make the same kind of argument for any operation at all: var Z int
var _ = Z == 0
var _ = Z + 1 == 0 What if you forget the It seems to me the issue is what if the programmer writes wrong code that still type-checks, and I would argue that we already don't guard against 99.99% of those cases.
Then why not add numbers and strings to that list, for the same reason? |
The list of 3 problems can be reduced to 2:
returning zeroes is just a case of 1, though surely the most common. I said nothing about generics because generics is not the only place these problems arise, though it's the most likely place this comes up now. For the purposes of this argument we can divide code into three classes, which I hope are obvious enough
In regular code naming zero comes up in cases where you need to write In generic code naming zero is much harder and comparing only works when the parameter is In generated code you have the same naming problem as with generic code and end up writing If this proposal is accepted, generic code is fixed. It would do nothing for naming in regular or generic code but it would allow definition of func isZero[T any](v T) bool {
return v == nil
} which could be used in regular/generated code to handle comparison. But it would need to be copied around all over the place and there would still be the other problems. The only way to extend it would be to generalize This proposal would be an improvement but not a very satisfying one. |
If this proposal is accepted, I think that an |
Understood. And yet I think that people write
Because we don't need to. There is a problem to solve for type parameters. There is no problem with numbers and strings. |
@ianlancetaylor What does the frequency of one vs. the other have to do with your argument that adding nil to every type would add more confusion to the language? My argument was that these "confusions" already exist in the language.
This is not persuasive, and frankly comes across as a brush off. If you think you've identified a root cause of disagreement that can't be resolved, then explain your reasoning.
This is only restating the proposal, that zero values outside of generic contexts shouldn't be addressed. I'm trying to better understand the trade-offs of this approach vs. #61372 in terms of complexity, which I assume you think is a relevant aspect to consider. |
@ianlancetaylor personally I'm sympathetic to this idea. But I'm leaning more toward zero because I don't really see a type parameter as an interface but as a placeholder which has nilable types in its typeset sometimes but not always. I think that overloading nil here would be equally confusing. The rule that the zero value for a type parameter is nil seems assuredly easy to understand, I agree. I think that zero should be the universal zero with some caveats in where it can be used (only assignments in order to zero out memory locations). Maybe that should be its own proposal I'm not sure. |
What I said was "Extending the use of zero to all types ... permits confusion when using a variable p of pointer type: we can write both p == zero and *p == zero, but they mean very different things." What I mean is: people can intend to write one but accidentally write the other. Today that can't happen. I don't mean that this is confusing in the sense of understanding what it means. I mean that people can make a mistake, and the compiler will no longer help them. For that kind of argument, it does matter that people compare pointers to |
@jimmyfrasche As an addendum, you can solve the naming problem with a function as well: func zero[T any]() T {
var z T
return z
} Right now, this doesn't help a lot for regular code (writing Of course, this has the same tradeoffs as |
I'll just add to this discussion that as written in the spec, the underlying type of a type parameter is its constraint, which is an interface (which is the RHS of a type parameter declaration, matching other type declarations). Thus, a type parameter can be seen as an interface, albeit with a dynamic type that is "fixed" at instantiation time (this could also be a workable if basic implementation of generics). Being able to assign nil to a type parameter would be in sync with the ability to assign nil to ordinary interfaces. The assignment would only clear the value, not the type (as that one is fixed). So there's some technical merit to this idea. Implementation-wise, if |
This comment was marked as resolved.
This comment was marked as resolved.
I think that we should not do this because that means one could write code like func someGenericFunc[T any](v *T) {
// some code
if v == nil || *v == nil {
// do stuff
}
// other code
} Until now you could be sure that if you compare something against I'm also worried that people will start use (and define) |
Thanks for the update - I was wondering about this when skimming go.dev/cl/520336. It seems subtle whether to consider this as "new evidence" vis-a-vis #61372. If it is "new evidence" (I think it's worth keeping track of...) I'd expect there will be more to consider in further details, and in code that starts using #61372 revealed a wide range of preferences, which can be frustrating for open decision-making. I think Osterhout may have been precisely aware of Arrow's theorem here - ranked or unstable preferences can make consensus impossible. Considering limited options can be tolerated, though, so I think this proposal is useful even if the inertia is with #61372. |
@DmitriyMV If we accept #61372 as is (use |
#22729 is still appealing to me, although it seems like it's dead at this point. It seems like if we did this, we would want to pair it with some version of #22729, so |
I support this. At least there should be a
|
#61327 has the same problem. If you take generic code and copy paste it, you would need to change |
I don't think so - I'm pretty sure that current |
If you take |
These are legal (if clever) patterns in non-generic code, and can lead to compound func f(t *any, ...) ... or type fn func()
func (f *fn) g(...) ... I guess I'm unsurprised that a compound |
In both of those cases double check for With
@carlmjohnson point taken. Although with But my main concern was mostly about the fact, that |
That is true. It's a different case that fails. func F[T int | string](v T) T {
return v + zero // Permitted because T supports + but has no specific name for the zero value.
} Copying that version of |
I agreed with that in my message above. But isn't this the same for this proposal? func F[T int | string](v T) T {
return v + T(nil) // or even v + nil
} |
@DmitriyMV Sorry, I was replying to #62487 (comment) and I missed that in the later comment #62487 (comment) you agreed that both proposals have the drawback that manually instantiating generic code can require modifying the code. Sorry for the noise. |
I'm reading the objection here as: it's novel that an untyped This isn't dispositive w/r/t the |
I am deeply conflicted on this. In favor: I really like the generalization of "this is the thing we use to zero everything". So I simultaneously love this and hate it and I don't know which is winning. |
For reference of anyone not following that thread in real time, #61372 has just been withdrawn. |
@seebs nit: slices are not pointer-shaped. |
@Merovius Slices are more pointer-shaped than interfaces containing non-pointer types. |
Retracting this along with #61372. We can take another pass at this later. |
This is an alternative to #61372. I mentioned it in #61372 (comment), and I'm here pulling it out into a separate proposal.
I propose that we permit using
nil
with type parameters. Specifically, I propose that we change the language such thatnil
to a variable whose type is a type parameter; it sets the variable to the zero value of its type;nil
; this is permitted even if the type parameter is not comparable, and==
reports whether the value is equal to the zero value of its type.Background
#61372 aims to solve three problems:
Referring to a zero value in generic code. Today people suggest
*new(T)
, which [@rsc finds] embarrasingly clunky to explain to new users. This comes up fairly often, and we need something cleaner.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 incmp.Or
(cmp: add Or #60204).Shortening error returns:
return zero, err
is nicer thanreturn time.Time{}, err
.The solution in #61372 is to introduce a new builtin type
zero
that may be used with any type that does not already have a short name for the zero value.This proposal addresses the first two points but not the third.
Rationale
I'm concerned that #61372 is overfitting to the current language and creating a solution that makes sense to experienced Go programmers but not to people learning the language for the first time. We already have three names for the zero value:
0
,""
,nil
. Introducing a fourth name,zero
, makes sense given where we are but seems confusing for people learning the language for the first time.Extending the use of
zero
to all types, as originally proposed and as mentioned by @robpike at #61372 (comment), permits confusion when using a variablep
of pointer type: we can write bothp == zero
and*p == zero
, but they mean very different things (as mentioned by @rsc at #61372 (comment)).Extending the use of
nil
to all types somewhat exacerbates the problem address by the FAQ in Why is my nil error value no equal to nil?, which has itself been reported as a bug many times despite the FAQ entry (#43643, #44102, #47551, #53768, #54229, #56930, #57292, #59521 among many others).Extending
nil
to type parameters only, as proposed here, is imperfect, but it addresses the immediate needs, and to me it seems less imperfect than the other solutions.The text was updated successfully, but these errors were encountered: