-
Notifications
You must be signed in to change notification settings - Fork 17.9k
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: disallow NaN keys in maps #20660
Comments
FWIW, there's a fair amount of code in the hashmap implementation that deals with the possibility of k!=k for keys k (search for reflexivekey in runtime/hashmap.go). It would be nice to strip that out. |
The strange behavior with maps only because == is not symmetric when applied on NaNs. |
@randall77 It may be that this proposal would simplify the hashmap implementation, but I would prefer to focus on the correctness and complexity implications for user code. |
@dsnet As @rsc notes in the blog post, allowing m := map[uint64]V
…
switch {
case math.IsNan(k):
m[math.Float64bits(math.NaN())] = v
case k == 0: // +0.0 or -0.0
m[math.Float64bits(0)] = v
default:
m[math.Float64bits(k)] = v
} |
@bcmills : I agree, just wanted to let people know what the effects on the implementation would be (tl;dr a small net positive). |
All and every value of type {float32,float64} is comparable (guaranteed by the specs). The result of the comparison is irrelevant here. Map keys must be comparable is a simple, non-complicated rule. I object to making it not only more complex, but suprisingly non-orthogonal. |
Not true. NaNs can be inside arrays or structs. All our current restrictions are type-based, not value-based. Even the interface panic restriction is about the type inside the interface, not the value. I'd really like to avoid introducing and explaining new value-based restrictions. |
Good point. But it's still easy to check, especially given the "not equal to itself" phrasing I've proposed: if k != k {
… // Can't be a map key. Do something else with it.
} else {
m[k] = v
} |
The main issue I want to address is the surprising inability to access elements by key in an irreflexively-keyed map. If you don't want to introduce value-based restrictions, perhaps we could at least change map entries from "with key equal to |
It's certainly an idea worth evaluating when we get to language changes again (hence the Go2 label). Like all the Go2-labelled things, I'm not going to try to evaluate them now. |
One other way to address the specific case of copying maps would be to extend the |
That would still give surprising behavior for functions that are not For example, consider this snippet: m := map[float64]int{k: 0}
m[k] += 1
m[k] += 1 It appears to increment the entry for Similarly, there is no way to delete a NaN entry from a map: https://play.golang.org/p/Rz2TLn336c8 So special-casing |
For an example where func inc(dst, src map[float64]int) {
for k, v := range src {
dst[k] += v
}
} There is currently no way to implement such a function correctly in the presence of NaNs: because |
I agree with @bcmills that a map with NaN key values is hard and perhaps even impossible to use. However, the current definition of maps is entirely type based, and I don't see a clear reason to change that. The current behavior is consistent and predictable even though surprising. @bcmills suggests that you can check for an irreflexive value before adding it to the map, but that is true whether we make this change or not. Closing. |
cc: griesemer |
I would argue that “surprising” is exactly the opposite of “predictable”: you can only predict the current behavior if you stop to think about it, and (empirically) most users don't. So what about using representation rather than equality (per #20660 (comment))? That would also be consistent and predictable, and would arguably be a better match for the behavior people already assume for maps with floating-point keys. (For example, it would give reasonable behavior for all of the examples of subtly-broken loops I've listed above.) The only downside I can see is that it would not equate the entries for A quick sample of float-keyed maps at Google (because that's an easy corpus for me to search) gives some interesting data:
|
Right now maps keys use the same operation as the |
I agree that minimizing special cases is a good thing, but I don't think it's accurate to say that the current implementation minimizes special cases. The existing semantics already imposes a special case: it just isn't documented. The spec for
But today, It's also worth noting that the current spec does not use the
If we summarize map semantics in terms of equality, the existing special cases become clear:
Using representation instead, the special cases disappear:
Enumerating the "equal" rules without the special cases gives the proposed panic semantics:
|
It's about the dynamic type of the value, which I think is still arguably a value-based restriction from the perspective of the map key type, even if the spec isn't currently worded to emphasize that. Currently we incompletely support NaN keys (e.g., That said, I'm not particularly fond of the representation-based approach. It feels like an abstraction violation to me, but that's an admittedly rather subjective concern. @ianlancetaylor This was tagged for Go2, but now it's closed. Does that mean it's been rejected for consideration from Go2? |
I don't think |
“Works in a way that is not useful to anybody” is the same as “doesn't work” in terms of user value. I have yet to see a program in which the current behavior of |
I don't have a strong preference between panicking on NaNs or using representations instead. If we decide to do neither and maintain the status quo, I do think we would need to address the other NaN-key issues in the standard library (#11104, #14427, and #24075 that I know of).
That's also true of a number of other run-time panics: for example, division by zero, out-of-bounds slice access, failed type-assertions, races on map operations, and attempts to put incomparable keys into interface-keyed maps. |
#43207 is another example of a package in the standard library erroneously assuming that map values can be looked up by key. |
When discussing the antireflexivity of NaN, I think it’s important to discuss why NaN is antireflexive. NaN is supposed to bubble out of mathematical operations. You have an invalid-decoded float, bad operation with an Infinity, etc. You did something bad and now you have a non-real or otherwise invalid float. So, all operations on NaN evaluate to NaN. (And if However, instead Go maps essentially hide NaNs away to never be seen again, they cannot be indexed nor deleted (they can only be detected by |
I've done a quick survey of map implementations and how they handle NaN keys in a number of other languages for reference:
|
There are only bad answers. |
To add on to @DeedleFake, here is JavaScript Map:
|
This comment thread on CL 338609 is another interesting data point. (It seems that the behavior of NaN map keys runs counter to the intuition of even @robpike!) |
I wouldn't say that. It was how NaNs worked (or not) in reflect.DeepEqual I didn't know. |
I don't like the idea of runtime panics on NaN being assigned to maps. Instead we should change the map behavior to allow NaN to be a valid key that can be queried by a NaN. |
IMO behind the scenes it should be doing something like what was proposed above for a workaround: if k == 0 { // -0.0 or +0.0
m[math.Float64bits(0)] = v
} else {
m[math.Float64bits(k)] = v
} |
I'd really like to avoid introducing NaNs, but I missed that boat. :( |
It's interesting that if we consider a value to be/have a subtype (if a type is a set of values, a subtype can be represented as a subset... e.g. float64 with the added constraint that the set of values is restricted to NaN), we can reproduce the same logic used for the I don't know if this would be backward-compatible though (because I think it would require some additional checks, equivalent to implementing a negated constraint such as non-reflexive comparability constraint checks). Could think about it. |
Just a note that as of the upcoming 1.21 release the language will have a new builtin, |
#63312 is another issue of a package in the standard library neglecting to document an exception to its behavior for NaN keys. The documentation for |
Panicking on The utility of being able to store multiple values in a hash with the "same" key, which you can't actually retrieve (without iterating over the whole map) or delete (without clearing the whole map) is very low. Anybody who currently does this, very likely does so by accident IMO. Special-casing NaN when used as a hash key sounds appealing at first, but it would also have to apply to NaNs deeply nested in structs. Nobody likes the idea of another line of code that could panic at runtime, but this is a similar case to interface comparisons, which are almost always allowed but can panic in certain cases (https://go.dev/play/p/eK9pF8naT8q) - or indeed a divide-by-zero error or a nil pointer dereference. I suppose a counter-argument is it might encourage more use of panic/recover in a try/catch kind of way, like this, but again, I don't suppose NaN map keys are used much. There is another possible behaviour: |
Motivation
Go currently supports NaN keys in maps through some clever tricks.
The current spec is ambiguous on this behavior. It says:
but "with key
x
" could plausibly mean "with key represented byx
". (In practice, today it means "with key equal tox
".)That leads to surprising and subtle bugs (such as #14427) and adds complexity even in correct code (#20138 (comment)).
For example, the following two functions appear to be equivalent, but the latter version silently copies zeroes instead of the intended values for NaN keys:
Proposal
We already reject incomparable values as map keys:
I propose that we should apply that standard to NaNs as well. Specifically, I propose that we add the following sentence to https://golang.org/ref/spec#Index_expressions:
Assigning to an element of a map with a key that is not equal to itself causes a run-time panic.
This proposal is along the lines of #19624, in that it produces panics for arithmetic operations which are otherwise likely to result in silent corruption in typical user code.
Workarounds
Users can easily avoid the proposed panics by
callingchecking whether the key is equal to itself before inserting a map entry.math.IsNaN(k)
Users who expect NaNs with the same representation to map to the same element can transform the keys using
math.Float64bits
ormath.Float32bits
:Users who really do intend to store all of the entries for NaN keys separately can explicitly pair the map with a slice:
The text was updated successfully, but these errors were encountered: