from Hacker News

Go 1.21 may have a clear(x) builtin

by aviramha on 11/19/22, 6:33 PM with 126 comments

  • by derriz on 11/19/22, 8:35 PM

    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".

  • by halpmeh on 11/19/22, 7:30 PM

    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.
  • by martisch on 11/19/22, 7:38 PM

    > 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

  • by inglor on 11/19/22, 7:40 PM

    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.
  • by ch_123 on 11/19/22, 8:07 PM

    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.
  • by _old_dude_ on 11/19/22, 8:32 PM

    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...

  • by karmakaze on 11/19/22, 10:29 PM

    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.
  • by akira2501 on 11/19/22, 8:14 PM

    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?
  • by zmj on 11/19/22, 8:15 PM

    C# had the same NaN behavior until recently: https://learn.microsoft.com/en-us/dotnet/core/compatibility/...
  • by chubot on 11/19/22, 9:06 PM

    Meh using floating point as keys for maps in any language is just asking for trouble -- it’s not just NaN

    I don’t think there’s any real use case for it

    I’d say clear() is good for clarity, and that’s it

  • by kazinator on 11/19/22, 10:10 PM

    This looks like catching up to GNU Awk, which allows "delete a" to clear an associative array. That's an extension over POSIX, which specifies "delete a[key]".
  • by kazinator on 11/19/22, 10:51 PM

    > This for loop is less efficient than clearing a map in one operation

    That is not obvious; a compiler could have a pattern match for that exact AST pattern, and transform it to a delete operation. (Except for that pesky issue where two fail to be equivalent due to NaN keys.)

    Quick and dirty, not entirely correct proof-of-concept in TXR Lisp:

      1> (macroexpand '(my-dohash (k v some.obj.hash) (print [some.obj.hash k])))
      (dohash (k v some.obj.hash
               ())
        (print [some.obj.hash
                 k]))
      2> (macroexpand '(my-dohash (k v some.obj.hash) (del [some.obj.hash k])))
      (clearhash hash)
    
    Impl:

      (defmacro my-dohash ((kvar vvar hash : result) . body)
        (if-match @(require ((del [@hash @kvar]))
                            (null result))
                  body
          ^(clearhash hash)
          ^(dohash (,kvar ,vvar ,hash ,result) ,*body)))
  • by yawaramin on 11/19/22, 8:31 PM

    OK, now I understand why OCaml's Float.compare function compares NaNs as equal to each other: https://discuss.ocaml.org/t/assertions-involving-nan/10762/4
  • by yencabulator on 11/20/22, 11:21 PM

    I'm more surprised they went with a new built-in instead of making the compiler recognize something like

      m = make(map[A]B, cap(m))
    
    just like it already recognized

        for k := range m {
            delete(m, k)
        }
    
    and many other similar idioms.

    (cap because len is not quite the same -- note, cap is not currently defined on maps)

    EDIT: Likely because that's an assigment on m, not a mutation of it, so it can't be done e.g. in a function that gets m as argument.

  • by unwind on 11/19/22, 8:49 PM

    That was weird. Makes sense once you know about NaN's incomparability, but still surprising.

    I had to try it in Python (3.10.4, Windows on x86), it worked fine:

        >>> import math
        >>> a={math.nan: 1}
        >>> a
        {nan: 1}
        >>> del(a[math.nan])
        >>> a
        {}
  • by kangalioo on 11/19/22, 8:27 PM

    Would anything actually break if NaN == NaN equalled true?
  • by anontrot on 11/20/22, 8:16 AM

    Hold on a sec, the whole clearing map scenario. Is it for reusing within your fn or is it trying to free memory? If latter, I’ve just been hoping GC does its job