What is the difference between “==” and “===” comparison operators in Julia?

@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 if x and y are the same object, stored at the same location in memory.
  • For immutable composite types, x === y is true if x and y 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 or Float64), x === y is true if x and y 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?

Leave a Comment

tech