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)