Readit News logoReadit News
derriz · 3 years ago
Java has a specific hack for this which I discovered by accident a few years ago. Normally, given (primitive) double values d0 and d1, then boxing them does not affect equality testing - i.e. d0 == d1 if and only if Double.valueOf(d0).equals(Double.valueOf(d1)). However if d0 and d1 are both NaN, then the boxed versions ARE considered equal while d0 == d1 is false.

This inconsistency infuriated me when I discovered it but the Javadoc for Double.equals explicitly states that this anomaly is there to "allow hash tables to work properly".

bobbylarrybobby · 3 years ago
Using floats as hash keys is insane, no?
zbentley · 3 years ago
Not at all.

If I get floats from $somewhere, I might want to index them into a hash data structure without creating a huge sparse array.

If you assume float numbers have "identity" such that, whatever the precision, "x == x" always holds (other than NaN), then this is a perfectly valid thing to want.

metadat · 3 years ago
Yes, why would you ever want a map with floating point keys?

I'm struggling to think of a valid use case where there is no better alternative.

Any design utilizing this language "feature" seems masochistic and begging to get sliced by sheet metal edges.

bushbaba · 3 years ago
Not if you’re hash table is designed to accommodate key conflicts, and the float is deterministic.
chrisseaton · 3 years ago
It's done all the time in JavaScript. Why not in Java?
vbezhenar · 3 years ago
No. JavaScript does that all the time.
xg15 · 3 years ago
Note that the contract for the equals() method requires that the method always returns true when called on itself. So the designers had to decide whether to break the contract on NaN or the contract on equals(). I could imagine that honoring equals() was the solution with the least expected breakage with downstream users.
halpmeh · 3 years ago
The issue with NaN equality is interesting, but is that really why they're adding a clear(x) builtin? What if you want to remove a single-NaN key from a map? clear(x) seems like a band-aid at best if the Go team is strictly trying to fix removing NaN keys in maps.
erk__ · 3 years ago
The way Rust dels with this issue is by saying that the Key must implement both `Hash` and `Eq` where Eq is a strict equality compared to `PartialEq` which allows the equality relation to be incomplete. While integers implement both PartialEq and Eq, floating point values only implement PartialEq.

https://doc.rust-lang.org/std/collections/struct.HashMap.htm...

https://doc.rust-lang.org/std/cmp/trait.Eq.html

https://doc.rust-lang.org/std/cmp/trait.PartialEq.html

There is also a partial ordering relation `PartialOrd` (and `Ord` for complete relations) Floating point values only implement the partial relation as well because of NaN's. This means that it is a bit harder to sort them, though the floating point standard also has a seperate complete comparison relation with some extra rules which you can use instead.

https://doc.rust-lang.org/std/primitive.f64.html#method.tota...

tialaramex · 3 years ago
Rust's HashMap and similar containers have clear() and many of them have drain()

Drain is interesting, because it resolves the other problem if things are in the collection which can never be retrieved from it. Drain gives you each of the things in the collection, one at a time, to do with as you wish, the collection is now empty. So this means even if the collection has a dozen SillyNonsenses in it, all of which insist they're not the one you were looking for whenever you go looking for a SillyNonsense in the collection, you get all twelve of them out in Drain, and can examine them as you wish.

Of course if you just wanted to fish one thing out and then throw the rest away, you can do that, once you drop the result of Drain the rest of the things being drained are dropped.

duped · 3 years ago
This is one of those rough edges where Rust is more annoying than helpful imo. It's also inconsistent.

I'd rather panic on NaN comparisons than complicate the fundamental comparison traits. Treat it like integer overflow or array bounds access. We don't have different traits for indexing that might fail or overflow that might happen. There are methods on the trait to handle those cases when you care about them.

But that said, this feels like an XY issue. If you're comparing hashes of floats as keys somewhere you don't want IEEE 754 defined equivalence. You almost certainly want bit equivalence, which means the keys should be type punned to u32/u64. Classic example is memoizing function calls with floats. You don't want to compare the semantic values of the arguments, but the literal bits. For floats that's not the same.

martisch · 3 years ago
> This for loop is less efficient than clearing a map in one operation

For maps with keys that are reflexive with == the Go compiler already optimizes the range loop to a single efficient runtime map clear call: https://go-review.googlesource.com/c/go/+/110055

tapirl · 3 years ago
Yes, for most cases, the for-loop is as efficient as the proposed "clear" function. I am not sure whether or not this is still true if there are NaN keys in the cleared map.
inglor · 3 years ago
Interestingly JavaScript handles this correctly and specified Object.is equality that is different than `===` equality around NaNs so `const m = new Map(); m.set(NaN, 3); m.get(NaN)` returns 3 and `.delete` similarly works.
ch_123 · 3 years ago
I'm curious why this is not implemented as a method on the map type (and others like list) instead of being a top-level builtin. I suppose it is consistent with other collection operations such as append and len... which I guess makes me wonder why those are builtins as well.
esprehn · 3 years ago
The answer is lack of generics in all previous versions of Go. There's no way to express a generic interface to have methods for map or slice (which are not interfaces, but compiler magic instead). All the operations on builtin types must be implemented as globals handled by the compiler.
hesdeadjim · 3 years ago
Been using Go since the early days, and 'make/append' always felt clunky and out of place. Especially when you could just use literals for a map/slice...
mseepgood · 3 years ago
Russ Cox explains in the issue why they don't want it to be a standard library function:

https://github.com/golang/go/issues/56351#issuecomment-12914...

"We talked about defining maps.Clear in #55002, but how? It would not be possible to write in pure Go code. Instead it would need some kind of hidden entry point into the runtime.

In the past I've referred to that kind of library change as a "back-door language change", meaning it's a language change - it adds something not possible in pure Go - but pretends not to be one. We try hard to avoid doing that."

urzaj · 3 years ago
From the go FAQ:

> Why is len a function and not a method?

> We debated this issue but decided implementing len and friends as functions was fine in practice and didn't complicate questions about the interface (in the Go type sense) of basic types.

I imagine similar reasoning applies to "clear" here.

https://go.dev/doc/faq#methods_on_basics

saghm · 3 years ago
That's a pretty weird rationale IMO. "It doesn't break anything" should be the minimum requirement for a major design choice, but it certainly isn't sufficient on its own. They sort of imply that making it a method would "complicate questions about the interface of basic types", but I don't really understand what that would even mean "in the Go type sense". Are they worried that someone might make an interface with a `Len` method and therefore be able to pass slices and maps to it? That honestly seems like a feature, not a bug, and certainly not worth throwing out the nicer syntax for.
AtNightWeCode · 3 years ago
Most obvious flaw in Go is the top-level bs. Why on earth are they adding things to that? It is off course down to other poor decisions made historically that forces this design. But anyway. They should fix the lang and scrap the top-level nonsense all together.

For a beginner in Go it makes absolutely no sense at all. Most of the builtins should be methods.

Deleted Comment

morelisp · 3 years ago
Why would they be implemented as methods when they can be functions?
_old_dude_ · 3 years ago
In Java, the primitive type double uses the IEEE-754 semantics and java.lang.Double uses the bits by bits comparison semantics [1] so List<Double>.remove() works correctly.

[1] https://docs.oracle.com/en/java/javase/19/docs/api/java.base...

uluyol · 3 years ago
This won't help you in a future version of Java where you can write List<double>, right?
masklinn · 3 years ago
No.

What will help you is that `clear()` is part of `Java.until.Collection` so just about every container has it.

_old_dude_ · 3 years ago
It depends if List<double> is an alias for List<@PleaseSpecialize @NotNull Double> or not.
karmakaze · 3 years ago
Usage of NaN as hash key reminds me of the two uses of NULL in SQL. One is as unknown values which are never equal to anything even other NULL values. Another use is as a key for aggregate grouping. In that case the entry represents the aggregate for all the unknown values which aren't equal but still grouped together. Different uses have different meanings so invalid in one use does not invalidate other uses.
akira2501 · 3 years ago
You've already got zero and negative zero as an outlying case. I'm not sure why anyone would feel comfortable using floats as keys in a map. To anyone who has done this.. why? What was the use case?
craigching · 3 years ago
Go has no built-in set data structure, you use map if you don’t want to bring in a third party. Maybe this is why?
Ferret7446 · 3 years ago
A set of floats sounds like an equally questionable idea as a map of float keys, for the exact same reason.

Deleted Comment

wallstop · 3 years ago
For a game I'm making, I have a helper function that takes in a specified time (as a float) and returns a (heap based) wait instruction. The times are literals sprinkled throughout the code base of callers of this function, so I maintain a cache of time -> wait instruction to reduce allocations.
iudqnolq · 3 years ago
Wouldn't your life be much simplier if you'd represented time as integer milliseconds? I totally get it being too late now, but I think that's an argument for making floats a little bit unpleasant to start working with.