Julia言語における中置演算子の扱い
はじめに
Julia言語の多重ディスパッチやJITコンパイル、実行速度については周知のところだと思います。
本記事で書きたいことは数学記号との相性の良さ、特に中置演算子の性質です。
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> 4 → 2 # 中置演算子として使える
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引数が負の整数の場合は特別扱いしよう!
- コードをparseした際に、
より詳細には、^
の第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
の例は極端で実用性が見えにくいですが、行列の積だと有り難さが見えやすいです。
行列の掛け算
実行例
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.
実行例
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 * c
はa + (b * c)
と同じ意味
は充たして欲しいですよね。つまり、中置演算子には記号自体に意味が込められています。
==
と類似のもの、つまりBool
を返すべき中置演算子には
in isa > < >= ≥ <= ≤ == === ≡ != ≠ !== ≢ ∈ ∉ ∋ ∌ ⊆ ⊈ ⊂ ⊄ ⊊ ∝ ∊ ∍ ∥ ∦ ∷ ∺ ∻ ∽ ∾ ≁ ≃ ≂ ≄ ≅ ≆ ≇ ≈ ≉ ≊ ≋ ≌ ≍ ≎ ≐ ≑ ≒ ≓ ≖ ≗ ≘ ≙ ≚ ≛ ≜ ≝ ≞ ≟ ≣ ≦ ≧ ≨ ≩ ≪ ≫ ≬ ≭ ≮ ≯ ≰ ≱ ≲ ≳ ≴ ≵ ≶ ≷ ≸ ≹ ≺ ≻ ≼ ≽ ≾ ≿ ⊀ ⊁ ⊃ ⊅ ⊇ ⊉ ⊋ ⊏ ⊐ ⊑ ⊒ ⊜ ⊩ ⊬ ⊮ ⊰ ⊱ ⊲ ⊳ ⊴ ⊵ ⊶ ⊷ ⋍ ⋐ ⋑ ⋕ ⋖ ⋗ ⋘ ⋙ ⋚ ⋛ ⋜ ⋝ ⋞ ⋟ ⋠ ⋡ ⋢ ⋣ ⋤ ⋥ ⋦ ⋧ ⋨ ⋩ ⋪ ⋫ ⋬ ⋭ ⋲ ⋳ ⋴ ⋵ ⋶ ⋷ ⋸ ⋹ ⋺ ⋻ ⋼ ⋽ ⋾ ⋿ ⟈ ⟉ ⟒ ⦷ ⧀ ⧁ ⧡ ⧣ ⧤ ⧥ ⩦ ⩧ ⩪ ⩫ ⩬ ⩭ ⩮ ⩯ ⩰ ⩱ ⩲ ⩳ ⩵ ⩶ ⩷ ⩸ ⩹ ⩺ ⩻ ⩼ ⩽ ⩾ ⩿ ⪀ ⪁ ⪂ ⪃ ⪄ ⪅ ⪆ ⪇ ⪈ ⪉ ⪊ ⪋ ⪌ ⪍ ⪎ ⪏ ⪐ ⪑ ⪒ ⪓ ⪔ ⪕ ⪖ ⪗ ⪘ ⪙ ⪚ ⪛ ⪜ ⪝ ⪞ ⪟ ⪠ ⪡ ⪢ ⪣ ⪤ ⪥ ⪦ ⪧ ⪨ ⪩ ⪪ ⪫ ⪬ ⪭ ⪮ ⪯ ⪰ ⪱ ⪲ ⪳ ⪴ ⪵ ⪶ ⪷ ⪸ ⪹ ⪺ ⪻ ⪼ ⪽ ⪾ ⪿ ⫀ ⫁ ⫂ ⫃ ⫄ ⫅ ⫆ ⫇ ⫈ ⫉ ⫊ ⫋ ⫌ ⫍ ⫎ ⫏ ⫐ ⫑ ⫒ ⫓ ⫔ ⫕ ⫖ ⫗ ⫘ ⫙ ⫷ ⫸ ⫹ ⫺ ⊢ ⊣ ⟂ ⫪ ⫫ <: >:
があります。
これはjulia-parser.scm
でprec-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 - 4
はa - (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.scm
でprec-plus
として定義されているためです。
まとめ
- Juliaでは中置演算子に使える記号があらかじめ決められている。
- 中置演算子を装飾して新しい中置演算子として使える。
- 中置演算子にも色々な種類があり、優先順位や結合性が決められている。
- Juliaの「メソッドの追加可能な中置演算子」と「多重ディスパッチ」は言語設計上相性が良い。
-
Discordの関連スレッドや公式ドキュメント(Customizable-binary-operators)、公式ドキュメント(Operator Precedence and Associativity)でもこのファイルを参照することが推奨されています。 ↩︎
-
入力の型が同一であれば出力の型も同一であること。例えば、
f(x) = if x > 0 1 else 0.0 end
のような関数は引数型が同一だったとしてもに1::Int
や0.0::Float64
が返ってくるので型安定ではありません。Juliaにおいては型安定なコードを書くことが高速化において非常に重要です。 ↩︎ -
literal_pow
の第1引数に^
が入っているのは少し不思議ですね。syntaxの定義では^
以外の中置演算子ではliteral_pow
を呼ばないようになっていますが、将来的に別の中置演算子をliteral_pow
でサポートすることもあるかも知れません。 ↩︎ -
パッケージ開発の経緯についてはIrrationalConstatns.jl#14も参照して下さい。 ↩︎
-
2つの実行例では同じサイズの行列を使っていますが、実行速度が大きく変わっています(825.050 μs vs 252.668 μs)。この理由は筆者はまだよく知りません。 ↩︎
-
この文法はjulia#22089のPRで作成されたものです。 ↩︎
-
完全に余談ですが、Wolfram言語の
Set
も同じような動作です。Pythonではa = (b = 4)
はエラーになります。 ↩︎ -
公式ドキュメントではPrecedence(優先順位)とAssociativity(結合性)の用語が使われていますが、定着した日本語があるかは筆者は知りません。 ↩︎
Discussion