Comparisons
Syntax#
- x < y # if
x
is strictly less thany
- x > y # if
x
is strictly greater thany
- x == y # if
x
is equal toy
- x === y # alternatively
x ≡ y
, ifx
is egal toy
- x ≤ y # alternatively
x <= y
, ifx
is less than or equal toy
- x ≥ y # alternatively
x >= y
, ifx
is greater than or equal toy
- x ≠ y # alternatively
x != y
, ifx
is not equal toy
- x ≈ y # if
x
is approximately equal toy
Remarks#
Be careful about flipping comparison signs around. Julia defines many comparison functions by default without defining the corresponding flipped version. For instance, one can run
julia> Set(1:3) ⊆ Set(0:5)
true
but it does not work to do
julia> Set(0:5) ⊇ Set(1:3)
ERROR: UndefVarError: ⊇ not defined
Chained Comparisons
Multiple comparison operators used together are chained, as if connected via the &&
operator. This can be useful for readable and mathematically concise comparison chains, such as
# same as 0 < i && i <= length(A)
isinbounds(A, i) = 0 < i ≤ length(A)
# same as Set() != x && issubset(x, y)
isnonemptysubset(x, y) = Set() ≠ x ⊆ y
However, there is an important difference between a > b > c
and a > b && b > c
; in the latter, the term b
is evaluated twice. This does not matter much for plain old symbols, but could matter if the terms themselves have side effects. For instance,
julia> f(x) = (println(x); 2)
f (generic function with 1 method)
julia> 3 > f("test") > 1
test
true
julia> 3 > f("test") && f("test") > 1
test
test
true
Let’s take a deeper look at chained comparisons, and how they work, by seeing how they are parsed and lowered into expressions. First, consider the simple comparison, which we can see is just a plain old function call:
julia> dump(:(a > b))
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol >
2: Symbol a
3: Symbol b
typ: Any
Now if we chain the comparison, we notice that the parsing has changed:
julia> dump(:(a > b >= c))
Expr
head: Symbol comparison
args: Array{Any}((5,))
1: Symbol a
2: Symbol >
3: Symbol b
4: Symbol >=
5: Symbol c
typ: Any
After parsing, the expression is then lowered to its final form:
julia> expand(:(a > b >= c))
:(begin
unless a > b goto 3
return b >= c
3:
return false
end)
and we note indeed that this is the same as for a > b && b >= c
:
julia> expand(:(a > b && b >= c))
:(begin
unless a > b goto 3
return b >= c
3:
return false
end)
Ordinal Numbers
We will look at how to implement custom comparisons by implementing a custom type, ordinal numbers. To simplify the implementation, we will focus on a small subset of these numbers: all ordinal numbers up to but not including ε₀. Our implementation is focused on simplicity, not speed; however, the implementation is not slow either.
We store ordinal numbers by their Cantor normal form. Because ordinal arithmetic is not commutative, we will take the common convention of storing most significant terms first.
immutable OrdinalNumber <: Number
βs::Vector{OrdinalNumber}
cs::Vector{Int}
end
Since the Cantor normal form is unique, we may test equality simply through recursive equality:
In version v0.5, there is a very nice syntax for doing this compactly:
import Base: ==
α::OrdinalNumber == β::OrdinalNumber = α.βs == β.βs && α.cs == β.cs
Otherwise, define the function as is more typical:
import Base: ==
==(α::OrdinalNumber, β::OrdinalNumber) = α.βs == β.βs && α.cs == β.cs
To finish our order, because this type has a total order, we should overload the isless
function:
import Base: isless
function isless(α::OrdinalNumber, β::OrdinalNumber)
for i in 1:min(length(α.cs), length(β.cs))
if α.βs[i] < β.βs[i]
return true
elseif α.βs[i] == β.βs[i] && α.cs[i] < β.cs[i]
return true
end
end
return length(α.cs) < length(β.cs)
end
To test our order, we can create some methods to make ordinal numbers. Zero, of course, is obtained by having no terms in the Cantor normal form:
const ORDINAL_ZERO = OrdinalNumber([], [])
Base.zero(::Type{OrdinalNumber}) = ORDINAL_ZERO
We can defined an expω
to compute ω^α
, and use that to compute 1 and ω:
expω(α) = OrdinalNumber([α], [1])
const ORDINAL_ONE = expω(ORDINAL_ZERO)
Base.one(::Type{OrdinalNumber}) = ORDINAL_ONE
const ω = expω(ORDINAL_ONE)
We now have a fully functional ordering function on ordinal numbers:
julia> ORDINAL_ZERO < ORDINAL_ONE < ω < expω(ω)
true
julia> ORDINAL_ONE > ORDINAL_ZERO
true
julia> sort([ORDINAL_ONE, ω, expω(ω), ORDINAL_ZERO])
4-element Array{OrdinalNumber,1}:
OrdinalNumber(OrdinalNumber[],Int64[])
OrdinalNumber(OrdinalNumber[OrdinalNumber(OrdinalNumber[],Int64[])],[1])
OrdinalNumber(OrdinalNumber[OrdinalNumber(OrdinalNumber[OrdinalNumber(OrdinalNumber[],Int64[])],[1])],[1])
OrdinalNumber(OrdinalNumber[OrdinalNumber(OrdinalNumber[OrdinalNumber(OrdinalNumber[OrdinalNumber(OrdinalNumber[],Int64[])],[1])],[1])],[1])
In the last example, we see that the printing of ordinal numbers could be better, but the result is as expected.
Standard Operators
Julia supports a very large set of comparison operators. These include
- All of the following unicode sequences:
> < >= ≥ <= ≤ == === ≡ != ≠ !== ≢ ∈ ∉ ∋ ∌ ⊆ ⊈ ⊂ ⊄ ⊊ ∝ ∊ ∍ ∥ ∦ ∷ ∺ ∻ ∽ ∾ ≁ ≃ ≄ ≅ ≆ ≇ ≈ ≉ ≊ ≋ ≌ ≍ ≎ ≐ ≑ ≒ ≓ ≔ ≕ ≖ ≗ ≘ ≙ ≚ ≛ ≜ ≝ ≞ ≟ ≣ ≦ ≧ ≨ ≩ ≪ ≫ ≬ ≭ ≮ ≯ ≰ ≱ ≲ ≳ ≴ ≵ ≶ ≷ ≸ ≹ ≺ ≻ ≼ ≽ ≾ ≿ ⊀ ⊁ ⊃ ⊅ ⊇ ⊉ ⊋ ⊏ ⊐ ⊑ ⊒ ⊜ ⊩ ⊬ ⊮ ⊰ ⊱ ⊲ ⊳ ⊴ ⊵ ⊶ ⊷ ⋍ ⋐ ⋑ ⋕ ⋖ ⋗ ⋘ ⋙ ⋚ ⋛ ⋜ ⋝ ⋞ ⋟ ⋠ ⋡ ⋢ ⋣ ⋤ ⋥ ⋦ ⋧ ⋨ ⋩ ⋪ ⋫ ⋬ ⋭ ⋲ ⋳ ⋴ ⋵ ⋶ ⋷ ⋸ ⋹ ⋺ ⋻ ⋼ ⋽ ⋾ ⋿ ⟈ ⟉ ⟒ ⦷ ⧀ ⧁ ⧡ ⧣ ⧤ ⧥ ⩦ ⩧ ⩪ ⩫ ⩬ ⩭ ⩮ ⩯ ⩰ ⩱ ⩲ ⩳ ⩴ ⩵ ⩶ ⩷ ⩸ ⩹ ⩺ ⩻ ⩼ ⩽ ⩾ ⩿ ⪀ ⪁ ⪂ ⪃ ⪄ ⪅ ⪆ ⪇ ⪈ ⪉ ⪊ ⪋ ⪌ ⪍ ⪎ ⪏ ⪐ ⪑ ⪒ ⪓ ⪔ ⪕ ⪖ ⪗ ⪘ ⪙ ⪚ ⪛ ⪜ ⪝ ⪞ ⪟ ⪠ ⪡ ⪢ ⪣ ⪤ ⪥ ⪦ ⪧ ⪨ ⪩ ⪪ ⪫ ⪬ ⪭ ⪮ ⪯ ⪰ ⪱ ⪲ ⪳ ⪴ ⪵ ⪶ ⪷ ⪸ ⪹ ⪺ ⪻ ⪼ ⪽ ⪾ ⪿ ⫀ ⫁ ⫂ ⫃ ⫄ ⫅ ⫆ ⫇ ⫈ ⫉ ⫊ ⫋ ⫌ ⫍ ⫎ ⫏ ⫐ ⫑ ⫒ ⫓ ⫔ ⫕ ⫖ ⫗ ⫘ ⫙ ⫷ ⫸ ⫹ ⫺ ⊢ ⊣
; - All symbols in point 1, preceded by a dot (
.
) to be made elementwise; - The operators
<:
,>:
,.!
, andin
, which cannot be preceded by a dot (.
).
Not all of these have a definition in the standard Base
library. However, they are available for other packages to define and use as appropriate.
In everyday use, most of these comparison operators are not relevant. The most common ones used are the standard mathematical functions for ordering; see the Syntax section for a list.
Like most other operators in Julia, comparison operators are functions and can be called as functions. For instance, (<)(1, 2)
is identical in meaning to 1 < 2
.
Using ==, ===, and isequal
There are three equality operators: ==
, ===
, and isequal
. (The last is not really an operator, but it is a function and all operators are functions.)
When to use ==
==
is value equality. It returns true
when two objects represent, in their present state, the same value.
For instance, it is obvious that
julia> 1 == 1
true
but furthermore
julia> 1 == 1.0
true
julia> 1 == 1.0 + 0.0im
true
julia> 1 == 1//1
true
The right hand sides of each equality above are of a different type, but they still represent the same value.
For mutable objects, like arrays, ==
compares their present value.
julia> A = [1, 2, 3]
3-element Array{Int64,1}:
1
2
3
julia> B = [1, 2, 3]
3-element Array{Int64,1}:
1
2
3
julia> C = [1, 3, 2]
3-element Array{Int64,1}:
1
3
2
julia> A == B
true
julia> A == C
false
julia> A[2], A[3] = A[3], A[2] # swap 2nd and 3rd elements of A
(3,2)
julia> A
3-element Array{Int64,1}:
1
3
2
julia> A == B
false
julia> A == C
true
Most of the time, ==
is the right choice.
When to use ===
===
is a far stricter operation than ==
. Instead of value equality, it measures egality. Two objects are egal if they cannot be distinguished from each other by the program itself. Thus we have
julia> 1 === 1
true
as there is no way to tell a 1
apart from another 1
. But
julia> 1 === 1.0
false
because although 1
and 1.0
are the same value, they are of different types, and so the program can tell them apart.
Furthermore,
julia> A = [1, 2, 3]
3-element Array{Int64,1}:
1
2
3
julia> B = [1, 2, 3]
3-element Array{Int64,1}:
1
2
3
julia> A === B
false
julia> A === A
true
which may at first seem surprising! How could the program distinguish between the two vectors A
and B
? Because vectors are mutable, it could modify A
, and then it would behave differently from B
. But no matter how it modifies A
, A
will always behave the same as A
itself. So A
is egal to A
, but not egal to B
.
Continuing along this vein, observe
julia> C = A
3-element Array{Int64,1}:
1
2
3
julia> A === C
true
By assigning A
to C
, we say that C
has aliased A
. That is, it has become just another name for A
. Any modifications done to A
will be observed by C
also. Therefore, there is no way to tell the difference between A
and C
, so they are egal.
When to use isequal
The difference between ==
and isequal
is very subtle. The biggest difference is in how floating point numbers are handled:
julia> NaN == NaN
false
This possibly surprising result is defined by the IEEE standard for floating point types (IEEE-754). But this is not useful in some cases, such as sorting. isequal
is provided for those cases:
julia> isequal(NaN, NaN)
true
On the flip side of the spectrum, ==
treats IEEE negative zero and positive zero as the same value (also as specified by IEEE-754). These values have distinct representations in memory, however.
julia> 0.0
0.0
julia> -0.0
-0.0
julia> 0.0 == -0.0
true
Again for sorting purposes, isequal
distinguishes between them.
julia> isequal(0.0, -0.0)
false