Welcome! Please see the About page for a little more info on how this works.

+2 votes
in Java Interop by

Consider the following code snippets evaluated with Clojure 1.11.1:

;; example 1
user=> (identical? Double/NaN Double/NaN)
false

;; example 2
user=> (let [x Double/NaN] (identical? x x))
false

;; example 3
user=> (identical? ##NaN ##NaN)
true

;; example 4
user=> (let [x ##NaN] (identical? x x))
false

Why does example 3 return true when all the other examples return false? Is this the intended behavior?

I assume this has something to do with with how Clojure sometimes uses unboxed math.

In my mental model for Clojure, identical? is basically Java's == and ##NaN is Java's Double.NaN. In Java, Double.NaN == Double.NaN evaluates to false. Thus I expected all the examples to return false.

2 Answers

+3 votes
by
 
Best answer

Okay, my mental model of identical? is not accurate. It calls clojure.lang.Util/identical which has the following implementation:

static public boolean identical(Object k1, Object k2){
    return k1 == k2;
}

When Clojure calls a function with such signature, it has to box the primitive values in the arguments. Doubles are boxed by calling Double.valueOf(double). The unusual equality semantics of NaN make the examples confusing, but in every case, identical? compares the reference equality of boxed values, not numerical equality.

By looking at disassembled byte code (see below), we can see what's happening. Here's summary.

  • In examples 1, 2, and 4, each of the arguments to identical? is boxed individually.
  • In example 3, ##NaN is boxed only once and the same boxed value is used as the both arguments of identical?.

Here are a few more examples. Note that the behavior of ##Inf matches that of ##NaN.

(identical? ##Inf ##Inf)          ; true
(let [x ##Inf] (identical? x x))  ; false

(identical? 1.0 1.0)              ; false
(let [x 1.0] (identical? x x))    ; false

(def my-one 1.0)
(identical? my-one my-one)        ; true

Conclusion

Everything works as it should. My mental model about identical? was wrong. It's more like (Object)... == (Object) ... than plain ==.

Lesson learned: Use identical? only for reference equality.

Disassembled examples

The code has been disassembled with clj-java-decompiler.

Example 1 (identical? Double/NaN Double/NaN)

class user$fn_line_1__254
    Minor version: 0
    Major version: 52
    Flags: PUBLIC, FINAL, SUPER
    
    public void <init>();
        Flags: PUBLIC
        Code:
                  linenumber      1
               0: aload_0        
               1: invokespecial   clojure/lang/AFunction.<init>:()V
               4: return         
    
    public static java.lang.Object invokeStatic();
        Flags: PUBLIC, STATIC
        Code:
                  linenumber      1
               0: getstatic       java/lang/Double.NaN:D
               3: invokestatic    java/lang/Double.valueOf:(D)Ljava/lang/Double;
                  linenumber      1
               6: getstatic       java/lang/Double.NaN:D
               9: invokestatic    java/lang/Double.valueOf:(D)Ljava/lang/Double;
                  linenumber      1
              12: invokestatic    clojure/lang/Util.identical:(Ljava/lang/Object;Ljava/lang/Object;)Z
              15: ifeq            24
              18: getstatic       java/lang/Boolean.TRUE:Ljava/lang/Boolean;
              21: goto            27
              24: getstatic       java/lang/Boolean.FALSE:Ljava/lang/Boolean;
              27: areturn        
        StackMapTable: 00 02 18 42 07 00 1D
    
    public java.lang.Object invoke();
        Flags: PUBLIC
        Code:
                  linenumber      1
               0: invokestatic    user$fn_line_1__254.invokeStatic:()Ljava/lang/Object;
               3: areturn        
    
    static {};
        Flags: PUBLIC, STATIC
        Code:
                  linenumber      1
               0: return         

Example 2 (let [x Double/NaN] (identical? x x))

Skipped; similar to example 4.

Example 3 (identical? ##NaN ##NaN)

Note that the boxing happens in the static block.

class user$fn_line_1__246
    Minor version: 0
    Major version: 52
    Flags: PUBLIC, FINAL, SUPER
    
    public static final java.lang.Object const__1;
        Flags: PUBLIC, STATIC, FINAL
    
    public void <init>();
        Flags: PUBLIC
        Code:
                  linenumber      1
               0: aload_0        
               1: invokespecial   clojure/lang/AFunction.<init>:()V
               4: return         
    
    public static java.lang.Object invokeStatic();
        Flags: PUBLIC, STATIC
        Code:
                  linenumber      1
               0: getstatic       user$fn_line_1__246.const__1:Ljava/lang/Object;
               3: getstatic       user$fn_line_1__246.const__1:Ljava/lang/Object;
                  linenumber      1
               6: invokestatic    clojure/lang/Util.identical:(Ljava/lang/Object;Ljava/lang/Object;)Z
               9: ifeq            18
              12: getstatic       java/lang/Boolean.TRUE:Ljava/lang/Boolean;
              15: goto            21
              18: getstatic       java/lang/Boolean.FALSE:Ljava/lang/Boolean;
              21: areturn        
        StackMapTable: 00 02 12 42 07 00 17
    
    public java.lang.Object invoke();
        Flags: PUBLIC
        Code:
                  linenumber      1
               0: invokestatic    user$fn_line_1__246.invokeStatic:()Ljava/lang/Object;
               3: areturn        
    
    static {};
        Flags: PUBLIC, STATIC
        Code:
                  linenumber      1
               0: ldc2_w          NaN
               3: invokestatic    java/lang/Double.valueOf:(D)Ljava/lang/Double;
               6: putstatic       user$fn_line_1__246.const__1:Ljava/lang/Object;
               9: return         

Example 4 (let [x ##NaN] (identical? x x))

Note two Double.valueOf calls in the invokeStatic method.

class user$fn_line_1__250
    Minor version: 0
    Major version: 52
    Flags: PUBLIC, FINAL, SUPER
    
    public void <init>();
        Flags: PUBLIC
        Code:
                  linenumber      1
               0: aload_0        
               1: invokespecial   clojure/lang/AFunction.<init>:()V
               4: return         
    
    public static java.lang.Object invokeStatic();
        Flags: PUBLIC, STATIC
        Code:
                  linenumber      1
               0: ldc2_w          NaN
               3: dstore_0        /* x */
               4: dload_0         /* x */
               5: invokestatic    java/lang/Double.valueOf:(D)Ljava/lang/Double;
               8: dload_0         /* x */
               9: invokestatic    java/lang/Double.valueOf:(D)Ljava/lang/Double;
                  linenumber      1
              12: invokestatic    clojure/lang/Util.identical:(Ljava/lang/Object;Ljava/lang/Object;)Z
              15: ifeq            24
              18: getstatic       java/lang/Boolean.TRUE:Ljava/lang/Boolean;
              21: goto            27
              24: getstatic       java/lang/Boolean.FALSE:Ljava/lang/Boolean;
              27: areturn        
        StackMapTable: 00 02 FC 00 18 03 42 07 00 1B
    
    public java.lang.Object invoke();
        Flags: PUBLIC
        Code:
                  linenumber      1
               0: invokestatic    user$fn_line_1__250.invokeStatic:()Ljava/lang/Object;
               3: areturn        
    
    static {};
        Flags: PUBLIC, STATIC
        Code:
                  linenumber      1
               0: return         
ago by
Interesting. My mental model of `identical?` was accurate (`identical?` returns true for the same object), but my mental model of Clojure's parameter passing was flawed. Looking at your examples, here's what should have happened, IMO.

```clojure
;; example 1
user=> (identical? Double/NaN Double/NaN)
false
;; false is probably correct. If Double/NaN was marked final, then should be true, but it apparently isn't, though Double class is marked final.

;; example 2
user=> (let [x Double/NaN] (identical? x x))
false
;; IMO the Clojure compiler should box once and pass the same box to identical?, thus true

;; example 3
user=> (identical? ##NaN ##NaN)
true
;; I'm glad this is true but surprised that this is optimized as a special case.

;; example 4
user=> (let [x ##NaN] (identical? x x))
false
;; As with example 2, the Clojure compiler should be smart enough to box this once, thus true.
```

Basically, my intuition is that if `x` is let-bound to a value, that identical object, whether boxed or unboxed, should be passed to a called function. As a pure matter of efficiency, not having anything to do with `identical?`, the compiler should not be boxing the same primitive value multiple times. So, IMO, this seems like an issue with the Clojure compiler not generating either intuitive or optimal code.
0 votes
by

In example 3, reader binds those two constants under the same Object constant, so there's just one instance of type Object.

And in other examples, there are instances of the double type.

With all that, it's just how Java works: https://stackoverflow.com/a/1408643/564509

An easy way to demonstrate the Java's behavior:

jshell> Object a = Double.NaN
a ==> NaN

jshell> a == a
$2 ==> true

jshell> double a = Double.NaN
a ==> NaN

jshell> a == a
$4 ==> false
by
How did you reach this conclusion?
by
If you mean the first two statements in my message, then I got your Clojure code, compiled it, decompiled it, and inspected the results.
by
Did you use clj-java-decompiler's decompile (Java output)? Try out disassemble (bytecode output) too; it doesn't really match your explanation.
by
I used the decompiler that's built into IntelliJ IDEA. But it shouldn't matter that much what you use. In the end, you can even inspect the byte code with some viewer. To increase the certainty, you can bind each expression in your OP to a name with `def`s.
...