Juliaでの等号と不等号いろいろ
TL;DR
-
isequalは==とも===とも違うので注意 -
islessは<とも≤とも違うので注意 - 一方で
isapproxは≈と同じだったりする
等号評価
実行例
Juliaには等号評価の演算として以下の3つの関数が用意されています。
=====isequal
これらの差異は以下のコードを実行して確認することができます。
julia> operators = [==, isequal, ===]
3-element Vector{Function}:
== (generic function with 178 methods)
isequal (generic function with 26 methods)
=== (built-in function)
julia> pairs = [(Inf, Inf), (Inf, Inf32), (+0.0,-0.0), (NaN, NaN), (NaN, -NaN), (Inf, -Inf)]
6-element Vector{Tuple{Float64, AbstractFloat}}:
(Inf, Inf)
(Inf, Inf32)
(0.0, -0.0)
(NaN, NaN)
(NaN, NaN)
(Inf, -Inf)
julia> [f(pair...) for f in operators, pair in pairs]
3×6 Matrix{Bool}:
1 1 1 0 0 0
1 1 0 1 1 0
1 0 0 1 0 0
基本的には==, isequal, ===の順で等号評価が厳格になっているようです。
次の節で詳細を解説します。
解説
-
==は 「数学的に等しいか」 の評価- 例えば
+0.0と-0.0は数学的にはどちらも単に なので等しいと評価されます。0 -
1/0よりも2/0の方が大きいということはなく、どちらのInfとして等しくなります。 -
NaNは特定の実数を近似した浮動小数点数ではないため、NaN == NaNはfalseです。[1]
- 例えば
-
===は 「ビット表現として厳密に等しいか」 の評価 -
isequalは 「オブジェクトが実質的に等しいか」 の評価-
NaNと-NaNを区別したくない場合に便利です。 - 一方で
isequal(+0.0, -0.0)はfalseなのでこれらは区別されます。[4]
-
julia> -NaN
NaN
julia> bitstring(NaN)
"0111111111111000000000000000000000000000000000000000000000000000"
julia> bitstring(-NaN)
"1111111111111000000000000000000000000000000000000000000000000000"
どの関数にどのようなメソッドを追加するべきか
Juliaには多重ディスパッチの仕組みがあるので、自前で定義した型の等号評価を定義できます。
では、どの関数にどのようなメソッドを追加するべきでしょうか?
-
===はbuilt-in関数なのでメソッドを追加できません。 -
==やisequalは上記の解説の方針に従ってメソッドを追加すればOKです。
例えばComplex(複素数)に対しては以下のようにメソッドが定義されています。
これらの関数の定義は@lessや@functionlocマクロを使って調べることができます。
REPLのhelpモードも便利です。
julia> @less ==(complex(1,2),complex(1,2))
julia> @functionloc ==(complex(1,2),complex(1,2))
("/home/hyrodium/.julia/juliaup/julia-1.9.3+0.x64.linux.gnu/share/julia/base/complex.jl", 243)
help?> ==
search: == === !==
==(x, y)
Generic equality operator. (長い説明。 略)
Juliaで追加するべきメソッドに迷った場合は、これらのマクロを使ってBase関数を調べたり、ヘルプを参照したりして実装の参考にすることができます。
ところで、これらのメソッドを追加しなくても==やisequalは使えるので、定義が不要な場合もあります。
julia> struct Hoge end
julia> Hoge() == Hoge()
true
julia> isequal(Hoge(), Hoge())
true
これはisequal(a::Any, b::Any)が==(a,b)にフォールバックされ、==(a::Any, b::Any)が===(a,b)にフォールバックされるようになっているためです。[5]
余談
inの評価には==が使われます。[6]
julia> NaN in [NaN]
false
julia> +0.0 in [-0.0]
true
isequalは==に等しくなかったですが、isapproxは≈に等しいです。
命名規則の一貫性として紛らわしいですが、isequal(と後述のisless)だけが例外と考えて問題ありません。
julia> (==) === isequal
false
julia> (≈) === isapprox # \approx<TAB>で≈が入力可能
true
不等号評価
実行例
Juliaには不等号評価のような演算として以下の3つの関数が用意されています。
<-
<=(≤) isless
これらの差異は以下のコードを実行して確認することができます。
julia> operators = [<, isless, ≤]
3-element Vector{Function}:
< (generic function with 74 methods)
isless (generic function with 43 methods)
<= (generic function with 55 methods)
julia> pairs = [(1, 2), (-0.0, +0.0), (+0.0, -0.0), (2, 1)]
4-element Vector{Tuple{Real, Real}}:
(1, 2)
(-0.0, 0.0)
(0.0, -0.0)
(2, 1)
julia> [f(pair...) for f in operators, pair in pairs]
3×4 Matrix{Bool}:
1 0 0 0
1 1 0 0
1 1 1 0
≤, isless, < の順で評価が厳しくなっているようですね。
解説
-
≤は 「左辺が右辺より数学的に小さいか、あるいは等しいか」 の評価[7]- これは簡単で、
1≤2や3≤3がtrueになるものです。
- これは簡単で、
-
<は 「左辺が右辺より数学的に小さいか」 の評価- これも簡単で、
1<2がtrueで3<3がfalseになるものです。
- これも簡単で、
-
islessは 「左辺が右辺より実質的に小さいか」 の評価- 基本的には
<と同じ挙動で、isless(1,2)がtrueでisless(3,3)がfalseです。 - しかし
isless(-0.0, +0.0)がtrueになります。 -
sortで標準的に使われます。
- 基本的には
julia> sort([4.2, +0.0, -0.0, -2.4, 3.2]) # +0.0と-0.0の順序が揃っていて気持ちいい
5-element Vector{Float64}:
-2.4
-0.0
0.0
3.2
4.2
julia> sort([4.2, +0.0, -0.0, -2.4, 3.2], lt=<) # <をlt(less than)に使うと揃わない
5-element Vector{Float64}:
-2.4
0.0
-0.0
3.2
4.2
searchsorted関数でもlt=islessがデフォルトなので少し紛らわしい場合があります。
julia> searchsorted([-2, -1, -0.0, 0.0, 4, 5, 6], 0) # 0が0.0にpromoteされ、0.0にisequalで等しい範囲を返す
4:4
julia> searchsorted([-2, -1, -0.0, 0.0, 4, 5, 6], 0, lt=<) # <で比較するので0.0に==で等しい範囲が返される
3:4
julia> searchsorted([-2, -1, 0.0, -0.0, 4, 5, 6], 0) # 引数がislessでsortされていないので正しく計算できない
5:4
julia> searchsorted([-2, -1, 0.0, -0.0, 4, 5, 6], 0, lt=<) # 引数が<でsortされているので正しく==で等しい範囲が返される
3:4
どの関数にどのようなメソッドを追加するべきか
最小限の実装ではislessにのみメソッドを追加すればOKです。
以下のように≤, ≥, <, >, isgreaterにfallbackされます。
浮動小数点数を扱う場合など、必要に応じして≤や<を再定義することが可能です。[8]
実装する順序構造が、通常の順序構造とは異なる場合、islessや≤にメソッドを追加しない方が良いこともあります。
関数の本来の動作を逸脱するようなメソッドはType-III piracyと呼ばれ、この場合は別の関数を用意してメソッドを定義することが推奨されるためです。
幸いにしてJuliaではUnicode文字の二項演算子が使えるので、適当な記号を選んでメソッドを定義しましょう!
-
https://yosuke-furukawa.hatenablog.com/entry/2018/01/30/174425 などが詳しいです。 ↩︎
-
https://discourse.julialang.org/t/various-equalities-of-nan/42649 にDiscourseの議論があります。 ↩︎
-
http://nmi.jp/2021-09-09-NaN などが詳しいです。 ↩︎
-
inv(+0.0)とinv(-0.0)が異なる(Inf,-Inf)なので区別したい気持ちがあります。後述ののislessも参照してください。 ↩︎ -
==を単純に===にfallbackしない方が良いという議論もあったりします。https://github.com/JuliaLang/julia/issues/4648 などを参照してください。 ↩︎ -
全順序(反射律・推移律・反対称律・全順序律を満たすもの)になっていて欲しいですが、そもそも
NaN ≤ NaNはfalseなので反射律すら満たしていません。 ↩︎ -
一方で
≥や>には自分でメソッドを追加するべきではありません。 ↩︎
Discussion