@ChrisRackauckas’s answer is accurate as far as it goes – i.e. for mutable objects. There’s a bit more to the issue than that, however, so I’ll elaborate a bit here.
The ===
operator (an alias for the is
function) implements Henry Baker’s EGAL predicate [1, 2]: x === y
is true when two objects are programmatically indistinguishable – i.e. you cannot write code that demonstrates any difference between x
and y
. This boils down to the following rules:
- For mutable values (arrays, mutable composite types),
===
checks object identity:x === y
is true ifx
andy
are the same object, stored at the same location in memory. - For immutable composite types,
x === y
is true ifx
andy
have the same type – and thus the same structure – and their corresponding components are all recursively===
. - For bits types (immutable chunks of data like
Int
orFloat64
),x === y
is true ifx
andy
contain exactly the same bits.
These rules, applied recursively, define the behavior of ===
.
The ==
function, on the other hand, is user-definable, and implements “abstract value equality”. Overloadability is one key difference:
- The
===
is not overloadable – it is a builtin function with fixed, pre-defined behavior. You cannot extend or change its behavior. - The
==
is overloadable – it is a normal (for Julia) generic function with infix syntax. It has fallback definitions that give it useful default behavior on user-defined types, but you can change that as you see fit by adding new, more specific methods to==
for your types.
To provide more detail about how ==
behaves for built-in types and how it should behave for user-defined types when people extend it, from the docs:
For example, all numeric types are compared by numeric value, ignoring
type. Strings are compared as sequences of characters, ignoring
encoding.
You can think of this as “intuitive equality”. If two numbers are numerically equal, they are ==
:
julia> 1 == 1.0 == 1 + 0im == 1.0 + 0.0im == 1//1
true
julia> 0.5 == 1/2 == 1//2
true
Note, however that ==
implements exact numerical equality:
julia> 2/3 == 2//3
false
These values are unequal because 2/3
is the floating-point value 0.6666666666666666
, which is the closest Float64
to the mathematical value 2/3 (or in Julia notation for a rational values, 2//3
), but 0.6666666666666666
is not exactly equal to 2/3. Moreover, ==
Follows IEEE 754 semantics for floating-point numbers.
This includes some possibly unexpected properties:
- There are distinct positive and negative floating-point zeros (
0.0
and-0.0
): they are==
, even though they behave differently and are thus not===
. - There are many different not-a-number (
NaN
) values: they are not==
to themselves, each other, or any other value; they are each===
to themselves, but not!==
to each other since they have different bits.
Examples:
julia> 0.0 === -0.0
false
julia> 0.0 == -0.0
true
julia> 1/0.0
Inf
julia> 1/-0.0
-Inf
julia> NaN === NaN
true
julia> NaN === -NaN
false
julia> -NaN === -NaN
true
julia> NaN == NaN
false
julia> NaN == -NaN
false
julia> NaN == 1.0
false
This is kind of confusing, but that’s the IEEE standard.
Further, the docs for ==
also state:
Collections should generally implement
==
by calling==
recursively on all contents.
Thus, the notion of value equality as given by ==
is extended recursively to collections:
julia> [1, 2, 3] == [1, 2, 3]
true
julia> [1, 2, 3] == [1.0, 2.0, 3.0]
true
julia> [1, 2, 3] == Any[1//1, 2.0, 3 + 0im]
true
Accordingly, this inherits the foibles of scalar ==
comparisons:
julia> a = [1, NaN, 3]
3-element Array{Float64,1}:
1.0
NaN
3.0
julia> a == a
false
The ===
comparison, on the other hand always tests object identity, so even if two arrays have the same type and contain identical values, they are only equal if they are the same array:
julia> b = copy(a)
3-element Array{Float64,1}:
1.0
NaN
3.0
julia> a === a
true
julia> a === b
false
julia> b === b
true
The reason that a
and b
are not ===
is that even though they currently happen to contain the same data here, since they are mutable and not the same object, you could mutate one of them and then it would become apparent that they are different:
julia> a[1] = -1
-1
julia> a # different than before
3-element Array{Int64,1}:
-1
2
3
julia> b # still the same as before
3-element Array{Int64,1}:
1
2
3
Thus you can tell that a
and b
are different objects through mutation. The same logic doesn’t apply to immutable objects: if they contain the same data, they are indistinguishable as long as they have the same value. Thus, immutable values are freed from the being tied to a specific location, which is one of the reasons that compilers are able to optimize uses of immutable values so effectively.
See Also:
- Get rid of Julia’s `WARNING: redifining constant` for strings that are not changed?