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