🍡

Julia言語における中置演算子の扱い

2023/02/27に公開

はじめに

Julia言語の多重ディスパッチやJITコンパイル、実行速度については周知のところだと思います。

https://twitter.com/bicycle1885/status/503513042554855424

https://twitter.com/bicycle1885/status/1293180133667303431

本記事で書きたいことは数学記号との相性の良さ、特に中置演算子の性質です。
Julia言語は科学技術計算向けの言語として開発された背景があり、とくにUnicodeを使った数学記法との相性が良いように設計されています。

その例:

julia> α₄ = 42  # \alpha<Tab>\_4<Tab> で入力
42

julia> 3α₄/12π  # 3 * 42 / (12 * π)
3.3422538049298023

julia> 8[1,8,12]  # 要素が入っているか。Unicodeの中置演算子(\in<Tab>)が便利!
true

julia> [2,4][1,8,12]  # 部分集合の判定。Unicodeの中置演算子(\subseteq<Tab>)が便利!
false

初級編

中置演算子は通常の関数のようにも使える

つまり、中置しなくてもOKです!

julia> +(1,2)  # 1 + 2 と同じ
3

julia> in(1,[1,2])  # 1 in [1,2] と同じ
true

julia>([1,42],[1,2])  # [1,42] ⊆ [1,2] と同じ
false

この辺りについては公式ドキュメントにも記載があります。

中置演算子に使用可能な記号

Juliaでは中置演算子に使用できる記号が決まっています。
以下の例を見てみましょう。

julia>(a, b) = a - b  # \to<Tab>で入力可能(generic function with 1 method)

julia> f(a, b) = a - b  # 同じ定義
f (generic function with 1 method)

julia> 42  # 中置演算子として使える
2

julia> 4 f 2  # ダメーッ!
ERROR: syntax: extra token "f" after end of expression

では、どのような記号が中置演算子として使えるのでしょうか?

正解は以下の648種類の記号です!

= += -= −= *= /= //= |\\=| ^= ÷= %= <<= >>= >>>= |\|=| &= ⊻= ≔ ⩴ ≕
~
:= $=
=>
?
← → ↔ ↚ ↛ ↞ ↠ ↢ ↣ ↦ ↤ ↮ ⇎ ⇍ ⇏ ⇐ ⇒ ⇔ ⇴ ⇶ ⇷ ⇸ ⇹ ⇺ ⇻ ⇼ ⇽ ⇾ ⇿ ⟵ ⟶ ⟷ ⟹ ⟺ ⟻ ⟼ ⟽ ⟾ ⟿ ⤀ ⤁ ⤂ ⤃ ⤄ ⤅ ⤆ ⤇ ⤌ ⤍ ⤎ ⤏ ⤐ ⤑ ⤔ ⤕ ⤖ ⤗ ⤘ ⤝ ⤞ ⤟ ⤠ ⥄ ⥅ ⥆ ⥇ ⥈ ⥊ ⥋ ⥎ ⥐ ⥒ ⥓ ⥖ ⥗ ⥚ ⥛ ⥞ ⥟ ⥢ ⥤ ⥦ ⥧ ⥨ ⥩ ⥪ ⥫ ⥬ ⥭ ⥰ ⧴ ⬱ ⬰ ⬲ ⬳ ⬴ ⬵ ⬶ ⬷ ⬸ ⬹ ⬺ ⬻ ⬼ ⬽ ⬾ ⬿ ⭀ ⭁ ⭂ ⭃ ⭄ ⭇ ⭈ ⭉ ⭊ ⭋ ⭌ ← → ⇜ ⇝ ↜ ↝ ↩ ↪ ↫ ↬ ↼ ↽ ⇀ ⇁ ⇄ ⇆ ⇇ ⇉ ⇋ ⇌ ⇚ ⇛ ⇠ ⇢ ↷ ↶ ↺ ↻ --> <-- <-->
||
&&
in isa
> < >= ≥ <= ≤ == === ≡ != ≠ !== ≢ ∈ ∉ ∋ ∌ ⊆ ⊈ ⊂ ⊄ ⊊ ∝ ∊ ∍ ∥ ∦ ∷ ∺ ∻ ∽ ∾ ≁ ≃ ≂ ≄ ≅ ≆ ≇ ≈ ≉ ≊ ≋ ≌ ≍ ≎ ≐ ≑ ≒ ≓ ≖ ≗ ≘ ≙ ≚ ≛ ≜ ≝ ≞ ≟ ≣ ≦ ≧ ≨ ≩ ≪ ≫ ≬ ≭ ≮ ≯ ≰ ≱ ≲ ≳ ≴ ≵ ≶ ≷ ≸ ≹ ≺ ≻ ≼ ≽ ≾ ≿ ⊀ ⊁ ⊃ ⊅ ⊇ ⊉ ⊋ ⊏ ⊐ ⊑ ⊒ ⊜ ⊩ ⊬ ⊮ ⊰ ⊱ ⊲ ⊳ ⊴ ⊵ ⊶ ⊷ ⋍ ⋐ ⋑ ⋕ ⋖ ⋗ ⋘ ⋙ ⋚ ⋛ ⋜ ⋝ ⋞ ⋟ ⋠ ⋡ ⋢ ⋣ ⋤ ⋥ ⋦ ⋧ ⋨ ⋩ ⋪ ⋫ ⋬ ⋭ ⋲ ⋳ ⋴ ⋵ ⋶ ⋷ ⋸ ⋹ ⋺ ⋻ ⋼ ⋽ ⋾ ⋿ ⟈ ⟉ ⟒ ⦷ ⧀ ⧁ ⧡ ⧣ ⧤ ⧥ ⩦ ⩧ ⩪ ⩫ ⩬ ⩭ ⩮ ⩯ ⩰ ⩱ ⩲ ⩳ ⩵ ⩶ ⩷ ⩸ ⩹ ⩺ ⩻ ⩼ ⩽ ⩾ ⩿ ⪀ ⪁ ⪂ ⪃ ⪄ ⪅ ⪆ ⪇ ⪈ ⪉ ⪊ ⪋ ⪌ ⪍ ⪎ ⪏ ⪐ ⪑ ⪒ ⪓ ⪔ ⪕ ⪖ ⪗ ⪘ ⪙ ⪚ ⪛ ⪜ ⪝ ⪞ ⪟ ⪠ ⪡ ⪢ ⪣ ⪤ ⪥ ⪦ ⪧ ⪨ ⪩ ⪪ ⪫ ⪬ ⪭ ⪮ ⪯ ⪰ ⪱ ⪲ ⪳ ⪴ ⪵ ⪶ ⪷ ⪸ ⪹ ⪺ ⪻ ⪼ ⪽ ⪾ ⪿ ⫀ ⫁ ⫂ ⫃ ⫄ ⫅ ⫆ ⫇ ⫈ ⫉ ⫊ ⫋ ⫌ ⫍ ⫎ ⫏ ⫐ ⫑ ⫒ ⫓ ⫔ ⫕ ⫖ ⫗ ⫘ ⫙ ⫷ ⫸ ⫹ ⫺ ⊢ ⊣ ⟂ ⫪ ⫫ <: >:
<|
|>
: .. … ⁝ ⋮ ⋱ ⋰ ⋯
$
+ - − ¦ |\|| ⊕ ⊖ ⊞ ⊟ |++| ∪ ∨ ⊔ ± ∓ ∔ ∸ ≏ ⊎ ⊻ ⊽ ⋎ ⋓ ⧺ ⧻ ⨈ ⨢ ⨣ ⨤ ⨥ ⨦ ⨧ ⨨ ⨩ ⨪ ⨫ ⨬ ⨭ ⨮ ⨹ ⨺ ⩁ ⩂ ⩅ ⩊ ⩌ ⩏ ⩐ ⩒ ⩔ ⩖ ⩗ ⩛ ⩝ ⩡ ⩢ ⩣
* / ⌿ ÷ % & · · ⋅ ∘ × |\\| ∩ ∧ ⊗ ⊘ ⊙ ⊚ ⊛ ⊠ ⊡ ⊓ ∗ ∙ ∤ ⅋ ≀ ⊼ ⋄ ⋆ ⋇ ⋉ ⋊ ⋋ ⋌ ⋏ ⋒ ⟑ ⦸ ⦼ ⦾ ⦿ ⧶ ⧷ ⨇ ⨰ ⨱ ⨲ ⨳ ⨴ ⨵ ⨶ ⨷ ⨸ ⨻ ⨼ ⨽ ⩀ ⩃ ⩄ ⩋ ⩍ ⩎ ⩑ ⩓ ⩕ ⩘ ⩚ ⩜ ⩞ ⩟ ⩠ ⫛ ⊍ ▷ ⨝ ⟕ ⟖ ⟗ ⨟
//
<< >> >>>
^ ↑ ↓ ⇵ ⟰ ⟱ ⤈ ⤉ ⤊ ⤋ ⤒ ⤓ ⥉ ⥌ ⥍ ⥏ ⥑ ⥔ ⥕ ⥘ ⥙ ⥜ ⥝ ⥠ ⥡ ⥣ ⥥ ⥮ ⥯ ↑ ↓

この一覧はsrc/julia-parser.scmから取得しました。[1]

与えられたUnicode文字が中置演算子(2項演算子)として使えるかはBase.isbinaryoperatorで調べることができます。[2]

julia> Base.isbinaryoperator(:÷)
true

julia> Base.isbinaryoperator(:(==))
true

julia> Base.isbinaryoperator(:$)
true

julia> Base.isbinaryoperator(:f)
false

中級編

特殊な中置演算子^

まずは次の演算結果を見てみましょう。

julia> ^(3, 2)  # 3の2乗は9
9

julia> ^(3, -1)  # 3の2乗は1/3
0.3333333333333333

julia> ^(3, 1-2)  # ^(3, -1)とは異なる!
ERROR: DomainError with -1:
Cannot raise an integer x to a negative power -1.
Make x or -1 a float by adding a zero decimal (e.g., 2.0^-1 or 2^-1.0 instead of 2^-1)or write 1/x^1, float(x)^-1, x^float(-1) or (x//1)^-1.

^(3, -1)^(3, 1-2)の実行結果が異なるのはかなり奇妙ですね。
これには以下のような背景があります。

  • ^(3, 2) (3^2) は整数9になって欲しい
  • 関数^は型安定[3]な方が好ましく、整数が引数である限りは整数を返すべき。
    • その理由で^(3, -1)は通常はエラーのはず。
  • しかし、型安定のためだけに^(3.0, -1)と書き直すのは面倒。
    • コードをparseした際に、^の第2引数が負の整数の場合は特別扱いしよう!

より詳細には、^の第2引数が(文字列として)整数の場合に^(a,b)literal_pow(^,a,Val(b))として処理されるようになっています。[4]

このliteral_powは、独自の数値型に対して2乗を定義する場面に特に役立ちます。

julia> Base.@irrational sqrt2 1.4142135623730950488 sqrt(big(2))  # 無理数√2の定義

julia> sqrt2  # この無理数の型はIrrational{:sqrt2}
sqrt2 = 1.4142135623730...

julia> sqrt2^2  # 数値誤差が発生
2.0000000000000004

julia> Base.literal_pow(::typeof(^),::Irrational{:sqrt2},::Val{2}) = 2

julia> sqrt2^2  # literal_powによって型安定な2乗が実現できる
2

この方法を使ったメソッドを定義するパッケージとしてIrrationalConstantsRules.jlもあります。(宣伝)[5]

中置演算子のメソッドを自分で定義する

実用的な例ではないですが、以下のようにして中置演算子にメソッドを定義できます。

julia> a ± b = (a+b, a-b)  # 左辺が中置演算子の形でもOK
± (generic function with 1 method)

julia> 1 ± 5  # 実行例①
(6, -4)

julia> a::Int ± b::Int = (a+b, a-b, "Int")  # 型の指定があってもOK
± (generic function with 2 methods)

julia> ±(a::Float64, b::Float64) = (a+b, a-b, "Float64")  # 普通の関数と同じ書き方の方が可読性は高い
± (generic function with 3 methods)

julia> 1 ± 5, 1. ± 5.  # 実行例②
((6, -4, "Int"), (6.0, -4.0, "Float64"))

julia> methods(±)  # メソッドの一覧の取得
# 3 methods for generic function "±":
[1] ±(a::Int64, b::Int64) in Main at REPL[3]:1
[2] ±(a::Float64, b::Float64) in Main at REPL[4]:1
[3] ±(a, b) in Main at REPL[1]:1

上記で左辺がa ± bの形でも良いと書きましたが、モジュール名まで入てa Base.:+ bのような左辺にするとエラーです。

julia> struct Point2
           x::Float64
           y::Float64
       end

julia> a::Point2 Base.:+ b::Point2 = Point2(a.x+b.x, a.y+b.y)  # Base.:+のように書くと中置演算子として使えない
ERROR: syntax: extra token "Base" after end of expression

julia> Base.:+(a::Point2, b::Point2) = Point2(a.x+b.x, a.y+b.y)  # 左辺が通常の関数の形であればOK

julia> Point2(1.0, 2.0) + Point2(8.0, -5.0)
Point2(9.0, -3.0)

左辺に中置演算子を含むと可読性が悪いので、素直に*(a, b) = ...function *(a, b) ... endの形でメソッドを定義しましょう。

上級編

複数の中置演算子

かなりトリッキーな例ですが、こんなことも可能です。

julia> Base.:+(a::Point2, b::Point2, c::Point2) = 42

julia> Point2(1,2) + Point2(3,4) + Point2(5,6)  # A+B+C の形ときにのみ呼ばれるメソッド
42

julia> (Point2(1,2) + Point2(3,4)) + Point2(5,6)
Point2(9.0, 12.0)

julia> Point2(1,2) + (Point2(3,4) + Point2(5,6))
Point2(9.0, 12.0)

このPoint2の例は極端で実用性が見えにくいですが、行列の積だと有り難さが見えやすいです。

行列の掛け算ABCにおいては、数学的には同じ値でも(AB)CA(BC)で計算速度が大きく違うことがあります。

A(BC)の方が速いケース

実行例
julia> A, B, C = rand(10,200), rand(200,10000), rand(10000,2);

julia> @benchmark ($A*$B)*$C  # ABを先に計算する方が遅い
BenchmarkTools.Trial: 1289 samples with 1 evaluation.
 Range (min … max):  3.020 ms …   5.842 ms  ┊ GC (min … max): 0.00%0.00%
 Time  (median):     3.773 ms               ┊ GC (median):    0.00%
 Time  (mean ± σ):   3.855 ms ± 473.692 μs  ┊ GC (mean ± σ):  0.25% ± 1.66%

            ▇█▅▃▆  ▂▁    ▁ ▁▁▁                                 
  ▃▄▅▃▃▂▄▃▇██████▇███▆█▇▇█▆███▆▆▇▅█▅▄▆▄▄▂▄▃▃▃▃▂▂▂▁▂▂▂▂▁▂▂▂▂▁▁ ▃
  3.02 ms         Histogram: frequency by time        5.29 ms <

 Memory estimate: 781.52 KiB, allocs estimate: 3.

julia> @benchmark $A*($B*$C)  # BCを先に計算する方が速い
BenchmarkTools.Trial: 4686 samples with 1 evaluation.
 Range (min … max):  826.362 μs …   1.840 ms  ┊ GC (min … max): 0.00%0.00%
 Time  (median):       1.044 ms               ┊ GC (median):    0.00%
 Time  (mean ± σ):     1.048 ms ± 132.109 μs  ┊ GC (mean ± σ):  0.00% ± 0.00%

     ▂▆▇▄▃       ▁▁▄▄▅▆▃▅█▅▅▄▅▄▃▁                                
  ▂▃▇██████▆█▇▇▆▇██████████████████▆▇▅▅▅▅▅▅▅▄▃▃▃▄▂▂▃▃▂▂▂▂▂▂▂▁▂▂ ▅
  826 μs           Histogram: frequency by time         1.42 ms <

 Memory estimate: 3.47 KiB, allocs estimate: 2.

julia> @benchmark $A*$B*$C  # いちばんはやい
BenchmarkTools.Trial: 4740 samples with 1 evaluation.
 Range (min … max):  825.050 μs …   1.664 ms  ┊ GC (min … max): 0.00%0.00%
 Time  (median):       1.030 ms               ┊ GC (median):    0.00%
 Time  (mean ± σ):     1.037 ms ± 130.141 μs  ┊ GC (mean ± σ):  0.00% ± 0.00%

     ▃▅█▆▃▂▃▂  ▁▂   ▃▁▃▂▃▃▆▆▄▂▁ ▂▁ ▁                             
  ▂▄▆█████████▇██████████████████████▇▇▆▇▆▅▅▅▅▅▄▃▃▄▃▃▃▃▂▂▂▂▂▁▂▁ ▅
  825 μs           Histogram: frequency by time         1.38 ms <

 Memory estimate: 3.47 KiB, allocs estimate: 2.

(AB)Cの方が速いケース[6]

実行例
julia> A, B, C = rand(2,10000), rand(10000,200), rand(200,10);

julia> @benchmark ($A*$B)*$C  # ABを先に計算する方が速い
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
 Range (min … max):  251.907 μs …  1.951 ms  ┊ GC (min … max): 0.00%0.00%
 Time  (median):     336.351 μs              ┊ GC (median):    0.00%
 Time  (mean ± σ):   372.046 μs ± 84.211 μs  ┊ GC (mean ± σ):  0.00% ± 0.00%

         █▇▂                                                    
  ▂▁▁▁▁▂████▆▅▄▄▃▂▃▂▂▂▂▂▂▃▅▇▆▆▄▃▃▂▂▂▂▂▁▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▂
  252 μs          Histogram: frequency by time          643 μs <

 Memory estimate: 3.47 KiB, allocs estimate: 2.

julia> @benchmark $A*($B*$C)  # BCを先に計算する方が遅い
BenchmarkTools.Trial: 3818 samples with 1 evaluation.
 Range (min … max):  1.088 ms …   2.539 ms  ┊ GC (min … max): 0.00%22.21%
 Time  (median):     1.231 ms               ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.290 ms ± 182.392 μs  ┊ GC (mean ± σ):  0.71% ±  3.72%

     ▃▇▆███▅▅▂▄▁▁                                              
  ▃▅▇████████████▇▆▄▃▄▃▃▃▃▃▃▃▂▂▃▂▂▂▂▂▂▂▂▂▁▂▂▂▁▂▁▂▂▂▂▂▂▂▁▂▂▁▂▁ ▃
  1.09 ms         Histogram: frequency by time        1.92 ms <

 Memory estimate: 781.52 KiB, allocs estimate: 3.

julia> @benchmark $A*$B*$C  # いちばんはやい
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
 Range (min … max):  252.668 μs …  1.243 ms  ┊ GC (min … max): 0.00%0.00%
 Time  (median):     316.449 μs              ┊ GC (median):    0.00%
 Time  (mean ± σ):   339.035 μs ± 63.503 μs  ┊ GC (mean ± σ):  0.00% ± 0.00%

         ▃██▃▁                                                  
  ▁▁▁▁▁▂▆█████▇▆▅▄▄▄▃▃▃▃▂▂▂▂▂▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▂
  253 μs          Histogram: frequency by time          600 μs <

 Memory estimate: 3.47 KiB, allocs estimate: 2.

ところで、このような定義は+*等のみに使えるもので、他の演算子には使えません。

julia> struct Foo end

julia> Foo()
Foo()

julia> Base.:*(a::Foo, b::Foo, c::Foo) = "foo"

julia>(a::Foo, b::Foo, c::Foo) = "foo"(generic function with 2 methods)

julia> Foo() * Foo() * Foo()
"foo"

julia> Foo() ⊗ Foo() ⊗ Foo()  # 定義したメソッドは呼ばれない
ERROR: MethodError: no method matching ⊗(::Foo, ::Foo)

これについては公式ドキュメントに記載されています。

The operators +, ++ and * are non-associative. a + b + c is parsed as +(a, b, c) not +(+(a, b), c). However, the fallback methods for +(a, b, c, d...) and *(a, b, c, d...) both default to left-associative evaluation.

中置演算子を自分でもっと定義する

中置演算子は648個しか無いと言ったな、あれは嘘だ。

Juliaでは中置演算子にsuffixをつけたものもまた中置演算子として使用可能です!

julia> +(a, b) = a+b+1  # ′はprime<Tab>で入力可能
+(generic function with 1 method)

julia> 8 +9
18

julia> +_1(a,b) = 3  # 使えないsuffixもある
ERROR: syntax: "_1(a, b)" is not a valid function argument name around REPL[1]:1

julia> Base.isbinaryoperator(:(+))  # isbinaryoperatorで確認してもtrue
true

suffixに使える文字の一覧は

₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎²³¹ʰʲʳʷʸˡˢˣᴬᴮᴰᴱᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾᴿᵀᵁᵂᵃᵇᵈᵉᵍᵏᵐᵒᵖᵗᵘᵛᵝᵞᵟᵠᵡᵢᵣᵤᵥᵦᵧᵨᵩᵪᶜᶠᶥᶦᶫᶰᶸᶻᶿ⁰ⁱ⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ⁿₐₑₒₓₕₖₗₘₙₚₛₜⱼⱽ" * "′″‴‵‶‷⁗

です!julia_opsuffs.hを参照してください。[7]

suffixは複数個重ねることが可能で、Unicodeの文字装飾と重ねることも可能です。

julia> +¹²³(a,b) = -(a+b)  # +\^123<tab>で入力
+¹²³ (generic function with 1 method)

julia> 8 +¹²³ 2
-10

julia> +̇(a,b) = 2(a+b)  # +\dot<tab>で入力
+̇ (generic function with 1 method)

julia> 1 +̇ 3
8

中置演算子の分類

中置演算子は明確に文法上の役割が分類されます。
例えば

  • a == b < c(a == b) & (b < c)と同じ意味
  • a + b * ca + (b * c)と同じ意味

は充たして欲しいですよね。つまり、中置演算子には記号自体に意味が込められています。
==と類似のもの、つまりBoolを返すべき中置演算子には

in isa > < >= ≥ <= ≤ == === ≡ != ≠ !== ≢ ∈ ∉ ∋ ∌ ⊆ ⊈ ⊂ ⊄ ⊊ ∝ ∊ ∍ ∥ ∦ ∷ ∺ ∻ ∽ ∾ ≁ ≃ ≂ ≄ ≅ ≆ ≇ ≈ ≉ ≊ ≋ ≌ ≍ ≎ ≐ ≑ ≒ ≓ ≖ ≗ ≘ ≙ ≚ ≛ ≜ ≝ ≞ ≟ ≣ ≦ ≧ ≨ ≩ ≪ ≫ ≬ ≭ ≮ ≯ ≰ ≱ ≲ ≳ ≴ ≵ ≶ ≷ ≸ ≹ ≺ ≻ ≼ ≽ ≾ ≿ ⊀ ⊁ ⊃ ⊅ ⊇ ⊉ ⊋ ⊏ ⊐ ⊑ ⊒ ⊜ ⊩ ⊬ ⊮ ⊰ ⊱ ⊲ ⊳ ⊴ ⊵ ⊶ ⊷ ⋍ ⋐ ⋑ ⋕ ⋖ ⋗ ⋘ ⋙ ⋚ ⋛ ⋜ ⋝ ⋞ ⋟ ⋠ ⋡ ⋢ ⋣ ⋤ ⋥ ⋦ ⋧ ⋨ ⋩ ⋪ ⋫ ⋬ ⋭ ⋲ ⋳ ⋴ ⋵ ⋶ ⋷ ⋸ ⋹ ⋺ ⋻ ⋼ ⋽ ⋾ ⋿ ⟈ ⟉ ⟒ ⦷ ⧀ ⧁ ⧡ ⧣ ⧤ ⧥ ⩦ ⩧ ⩪ ⩫ ⩬ ⩭ ⩮ ⩯ ⩰ ⩱ ⩲ ⩳ ⩵ ⩶ ⩷ ⩸ ⩹ ⩺ ⩻ ⩼ ⩽ ⩾ ⩿ ⪀ ⪁ ⪂ ⪃ ⪄ ⪅ ⪆ ⪇ ⪈ ⪉ ⪊ ⪋ ⪌ ⪍ ⪎ ⪏ ⪐ ⪑ ⪒ ⪓ ⪔ ⪕ ⪖ ⪗ ⪘ ⪙ ⪚ ⪛ ⪜ ⪝ ⪞ ⪟ ⪠ ⪡ ⪢ ⪣ ⪤ ⪥ ⪦ ⪧ ⪨ ⪩ ⪪ ⪫ ⪬ ⪭ ⪮ ⪯ ⪰ ⪱ ⪲ ⪳ ⪴ ⪵ ⪶ ⪷ ⪸ ⪹ ⪺ ⪻ ⪼ ⪽ ⪾ ⪿ ⫀ ⫁ ⫂ ⫃ ⫄ ⫅ ⫆ ⫇ ⫈ ⫉ ⫊ ⫋ ⫌ ⫍ ⫎ ⫏ ⫐ ⫑ ⫒ ⫓ ⫔ ⫕ ⫖ ⫗ ⫘ ⫙ ⫷ ⫸ ⫹ ⫺ ⊢ ⊣ ⟂ ⫪ ⫫ <: >:

があります。
これはjulia-parser.scmprec-comparisonとして定義されており、同様に、+の類似物はprec-plusとして同ファイルで定義されています。

false == 4 isa Boolの挙動を初めて見たとき、私は少し混乱しましたがprec-comparisonに属するものは同様にparseされると思えば自然だと思えるうようになりました。

julia> (false == 4) isa Bool  # これも
true

julia> false == (4 isa Bool)  # これもtrueだが
true

julia> false == 4 isa Bool  # こっちはfalse。
false

julia> (false == 4) && (4 isa Bool)  # このように考えればOK
false

中置演算子の優先順位

Juliaではa = b = 4のように変数を定義できますが、この定義においては右側の結合が優先され実はa = (b = 4)と書いてもOKです。[8]
一方で、a - b - 4a - (b - 4)ではなく(a - b) - 4に等しいです。
これの結合の強さはBase.operator_associativityで調べることができます。

julia> a = b = 4
4

julia> a = (b = 4)
4

julia> a - b - 4
-4

julia> a - (b - 4)
4

julia> (a - b) - 4
-4

julia> Base.operator_associativity(:(=))
:right

julia> Base.operator_associativity(:(-))
:left

ところで、結合の優先順位と言えば「足し算+よりも掛け算*の方が優先」のような文脈もあります。[9]
こちらについてはBase.operator_precedenceで調べることができます。

julia> Base.operator_precedence(:+), Base.operator_precedence(:*), Base.operator_precedence(:)  # 最後のは\oplus
(11, 12, 11)

数字の大きい方が強いことが分かりますね。
+とでまったく同じ結合度であることが分かりますね。
これはどちらもjulia-parser.scmprec-plusとして定義されているためです。

まとめ

  • Juliaでは中置演算子に使える記号があらかじめ決められている。
  • 中置演算子を装飾して新しい中置演算子として使える。
  • 中置演算子にも色々な種類があり、優先順位や結合性が決められている。
  • Juliaの「メソッドの追加可能な中置演算子」と「多重ディスパッチ」は言語設計上相性が良い。
脚注
  1. Discordの関連スレッド公式ドキュメント(Customizable-binary-operators)公式ドキュメント(Operator Precedence and Associativity)でもこのファイルを参照することが推奨されています。 ↩︎

  2. isbinaryoperatorの関連PR ↩︎

  3. 入力の型が同一であれば出力の型も同一であること。例えば、f(x) = if x > 0 1 else 0.0 endのような関数は引数型が同一だったとしてもに1::Int0.0::Float64が返ってくるので型安定ではありません。Juliaにおいては型安定なコードを書くことが高速化において非常に重要です。 ↩︎

  4. literal_powの第1引数に^が入っているのは少し不思議ですね。syntaxの定義では^以外の中置演算子ではliteral_powを呼ばないようになっていますが、将来的に別の中置演算子をliteral_powでサポートすることもあるかも知れません。 ↩︎

  5. パッケージ開発の経緯についてはIrrationalConstatns.jl#14も参照して下さい。 ↩︎

  6. 2つの実行例では同じサイズの行列を使っていますが、実行速度が大きく変わっています(825.050 μs vs 252.668 μs)。この理由は筆者はまだよく知りません。 ↩︎

  7. この文法はjulia#22089のPRで作成されたものです。 ↩︎

  8. 完全に余談ですが、Wolfram言語のSetも同じような動作です。Pythonではa = (b = 4)はエラーになります。 ↩︎

  9. 公式ドキュメントではPrecedence(優先順位)とAssociativity(結合性)の用語が使われていますが、定着した日本語があるかは筆者は知りません。 ↩︎

GitHubで編集を提案

Discussion