🟰

Juliaでの等号と不等号いろいろ

2023/09/10に公開

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 == NaNfalseです。[1]
  • ===「ビット表現として厳密に等しいか」 の評価
    • NaNのビット表現は一意的でないのでNaN === -NaNfalseになったりします。(後述のbitstringの例を参照)[2][3]
  • 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(複素数)に対しては以下のようにメソッドが定義されています。

https://github.com/JuliaLang/julia/blob/7d0da584f41664afa228dbbf608f41adc4190157/base/complex.jl#L244
https://github.com/JuliaLang/julia/blob/7d0da584f41664afa228dbbf608f41adc4190157/base/complex.jl#L248

これらの関数の定義は@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]

https://github.com/JuliaLang/julia/blob/7d0da584f41664afa228dbbf608f41adc4190157/base/Base.jl#L159
https://github.com/JuliaLang/julia/blob/7d0da584f41664afa228dbbf608f41adc4190157/base/operators.jl#L133

余談

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

https://github.com/JuliaLang/julia/blob/7d0da584f41664afa228dbbf608f41adc4190157/base/floatfuncs.jl#L322

不等号評価

実行例

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≤23≤3trueになるものです。
  • <「左辺が右辺より数学的に小さいか」 の評価
    • これも簡単で、1<2true3<3falseになるものです。
  • isless「左辺が右辺より実質的に小さいか」 の評価
    • 基本的には<と同じ挙動で、isless(1,2)trueisless(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]

https://github.com/JuliaLang/julia/blob/7d0da584f41664afa228dbbf608f41adc4190157/base/operators.jl#L352

https://github.com/JuliaLang/julia/blob/7d0da584f41664afa228dbbf608f41adc4190157/base/operators.jl#L378

https://github.com/JuliaLang/julia/blob/7d0da584f41664afa228dbbf608f41adc4190157/base/operators.jl#L401-L402

https://github.com/JuliaLang/julia/blob/7d0da584f41664afa228dbbf608f41adc4190157/base/operators.jl#L425-L426

https://github.com/JuliaLang/julia/blob/7d0da584f41664afa228dbbf608f41adc4190157/base/operators.jl#L232

実装する順序構造が、通常の順序構造とは異なる場合、islessにメソッドを追加しない方が良いこともあります。
関数の本来の動作を逸脱するようなメソッドはType-III piracyと呼ばれ、この場合は別の関数を用意してメソッドを定義することが推奨されるためです。
幸いにしてJuliaではUnicode文字の二項演算子が使えるので、適当な記号を選んでメソッドを定義しましょう!

脚注
  1. https://yosuke-furukawa.hatenablog.com/entry/2018/01/30/174425 などが詳しいです。 ↩︎

  2. https://discourse.julialang.org/t/various-equalities-of-nan/42649 にDiscourseの議論があります。 ↩︎

  3. http://nmi.jp/2021-09-09-NaN などが詳しいです。 ↩︎

  4. inv(+0.0)inv(-0.0)が異なる(Inf, -Inf)なので区別したい気持ちがあります。後述ののislessも参照してください。 ↩︎

  5. ==を単純に===にfallbackしない方が良いという議論もあったりします。https://github.com/JuliaLang/julia/issues/4648 などを参照してください。 ↩︎

  6. https://github.com/JuliaLang/julia/issues/9381 に議論があります。 ↩︎

  7. 全順序(反射律・推移律・反対称律・全順序律を満たすもの)になっていて欲しいですが、そもそもNaN ≤ NaNfalseなので反射律すら満たしていません。 ↩︎

  8. 一方で>には自分でメソッドを追加するべきではありません。 ↩︎

GitHubで編集を提案

Discussion