TL;DR
タグでネタバレしていますが、この記事はコードゴルフネタであり、決して Julia の実用的な記事ではないかもしれません。
それをご承知の上でそれでも OK という方だけ続きをご覧ください(そういうネタは不要という方はこのままそっ閉じしてください)。
初めに
この記事は、Julia Advent Calendar 2024 の「シリーズ2」8日目の記事です(2枚目ができて空いているので埋めさせていただきます)。
またこの記事は、このカレンダーの日 2024/12/08 にオンラインで行われた JuliaLangJa 年末 LT 大会 2024 での LT 発表内容を元にしております。その時の LT 資料が以下になります↓
これを(LTスライド使い回して)社内年末 LT でも発表したところ、そこそこ好評だったので、その時の反応も加えて記事として再構成してみました。
所々に引用形式の部分があります、これが「実際に社内LTしたときに出た反応」です(名前は伏せています)。社内 LT の聴講者は Julia に詳しくない方もおり、一方で他言語に精通している方もおり、JuliaLangJa の LT 大会で一度あの発表を聞いている方も「なるほどこういう感想・リアクションもあるんだ」といった別の気付きがあるかもしれません。
それでは、お楽しみください!
あなたは Julia のこんな書き方、知っていますか?
これから、「実は Julia ってこんな書き方ができるんですよ」、という例をいくつか紹介します。
Julia はよく書いているという腕に覚えのある方はもちろん、「Julia のことよく知らない」という方も「こういうことじゃないのかな?」と予想しながら見てみてください!
① ~=◯◯
まずはこれ。~=◯◯
というコードがあった場合、これは Julia ではどのような挙動となるでしょう?
A: 配列への追加(D)?
B: 左辺と右辺のビット演算結果を左辺へ代入?
C: Vim script では文字列の正規表現マッチ
あ、出ましたね、正規表現マッチ。Vim script もなんですね。Perl とか Ruby を知ってる人なら「知ってる正規表現マッチでしょ」って反応が返ってくるのは想定の範囲内です。でも正規表現マッチって ~=
じゃなくて =~
ですよね(少なくとも Perl や Ruby は)。
C: Vim script も
=~
だった...
D: じゃあ戻り値を捨てるで
正解が出ないようなので、答えを言います。
① ~=◯◯
:演算子への代入!
これ、実は ~
という演算子に ◯◯
を代入しているだけなんです!
演算子記号、これが単に有効な識別子なんですね、そこに何かを代入できてしまう、ということ。
B: なん…だと…
E: overloadとは違う?
いいえ、overload ではありません。「上書き」になります。
正確には上書きというか、ローカルスコープの新しい識別子として ~
が認識されるだけで、Base.:~
とすれば元の演算子は参照できます。
C: シャドーイングか
そう、それ。シャドウイングです。
ただ Julia でもこれは決してお行儀の良いことではないので、既存の演算子に対してはやるとしてもローカルスコープで一時的にしかやらないのが吉ですね。
例えばこんなコード例を:
let ~=+
2~3 # == 2 + 3
end
#> 5
let ~=binomial
(2:8).~2 # == binomial.(2:8, 2)
end
#> [1, 3, 6, 10, 15, 21, 28] # 三角数
let
ブロックでローカルスコープを作っているので、外部では ~
は本来の演算子のままなので安全です。
あと、代入しているのは他の演算子または関数。そうするとその演算子を新しい演算子として(代入した演算子や関数の機能が使えるものとして)使えます。~=+
なら以降加算演算子として使えますし、~=binomial
なら二項係数が計算できます。
他の例:
for~=(+,-,*,/)
println(2~3)
end
## 5
## -1
## 6
## 0.6666666666666666
let~=split,s="1 2 3\n4 5 6\n7 8 9"
stack(.~(s~'\n'), dims=1)
end
#> 3×3 Matrix{SubString{String}}:
#> "1" "2" "3"
#> "4" "5" "6"
#> "7" "8" "9"
Julia の for
はローカルスコープを作るので、こういう書き方もできます。
あと ~=split
とすると、~s
と書くと1引数関数 splir(s)
と同じで「デフォルトのデリミタ(=空白文字類)での split」となり、s~◯
と書くと2引数関数 splir(s, ◯)
と同じで「◯
をデリミタとして split」となります。なので ↑ のように書くと結果的に 3x3 の文字列(部分文字列)の2次元配列が得られます。
D: あまりにも見た目が黒魔術
D: これ実用してる方いるんですか?
いやまずいないでしょう。コードゴルフ やってる人以外はw
先ほどの ~=split
とするのは、Julia における コードゴルフの基本テクニックですw
D: 草
C: おもしろ
ということでお気づきの方も多いと思われますが、この LT は「主にコードゴルフでは役に立つかもしれない(けれど実用的では決してないかもしれない)ネタ」を発表する LT です!
例えば先ほどから ~=
と間にホワイトスペース空けてないですが、これは ~=
という複合代入演算子(更新演算子)が存在しないから許されています。ついでに言うと演算子の場合予約語のすぐ後に来ても識別できるのでこのコード例のように for~=…
とか let~=…
のように書いてもエラーになりません。これも普通はお行儀悪い書き方で、「余分な空白すら削りたい!」というコードゴルフでしか有用でないものですよねw
B: 質問:じゃあ例えば
a += b
とかも Julia では書けない?
あ、そうではありません。+=
という複合代入演算子は存在します。演算子 ~
に対する ~=
という演算子(複合代入演算子)は予約されていません。なので ~=◯◯
と書いて代入ができます。
逆に ★=
という形式の書き方が予約されている演算子 ★
に対しては ★=◯◯
と書いての直接代入はできません、parse時に区別ができないので。その代わり、実はこちらも ★ =◯◯
のように「★
」と「=
」の間にスペース空ければ代入できます(例:+ = *
で加算演算子に乗算演算子を代入)。
② a>b=◯◯
次はこれ。今度は a>b=◯◯
というコード。これは何でしょう?
E: bにintを代入しつつaをその分シフトする
D: bに代入してa>bを返す
C:=
の優先度が低いので a>OO と同じ意味で b=OO の代入も同時に行われる?
B: a>b && b==○○の糖衣構文?
あー、B:、おしいですね、a>b==◯◯
なら a>b && b==◯◯
と同じ意味になります。Python にもある 比較演算子の Chain ですね。でもこれは違います。a>b=◯◯
です。
あと D: と C:、Ruby とかならそうですね、そういう動きになります。でも Julia は違います。
こちらも正解が出ないようなので答え言います。
② a>b=◯◯
:演算の定義!
これ、実は「a>b
という演算は ◯◯
だよ」という定義式になっています!
コード例を見てください。
let
a > b = a % b == 0
6 .> (1:6)
end
#> [true, true, true, false, false, true]
let
a>b=a+2b
6 .> (1:6)
end
#> [8, 10, 12, 14, 16, 18]
例えば a > b = a % b == 0
と書くと「a > b
という演算は a
が b
で割り切れるかを判定する」という定義になります。なので 6 .> (1:6) == [true, true, true, false, false, true]
となります。
あと演算子が比較演算子だからって Bool
を返さなくてはならないという制約はありません。2つめの例のように普通の数値演算の結果を返すように定義することもOKです。
B: a>b は2引数演算子 > という意味なのか・・・。
C: a, b は仮引数なのか
はいお二方その通りです!
(一部の例外を除いて)Julia の 演算子は実は関数 です。これは Julia で(演算子を利用した)関数定義として Valid な書式なのです。
E: こっちは素直な演算子オーバーロードっぽい?
いえ、これも実はオーバーロードではなくシャドウイングです。
きちんとした演算子オーバーロードの仕方は別にあるんですが、LT がライトじゃなくなるので省きますw(註:後述)
で、これが何の役に立つのかというと、次のコード例を見てください。
# 1以上N未満のフィボナッチ数を列挙(1行ずつ出力)
let N=100
n>r=println(r.den)≠r.num<n>1+1/r
N>1//1
end
## 1
## 1
## 2
## 3
## 5
## 8
## 13
## 21
## 34
## 55
## 89
はい、フィボナッチ数の列挙がこんな感じで短く書けますw
n>r=◯◯
という定義中に >
演算子が出てきていますよね、これは有効で、再帰関数 の定義になります。
あとこれ、先ほどちらっと説明した 比較演算子のChain を利用していて、r.num<n
が成立するときだけ n>1+1/r
が実行されます。つまり「再帰呼び出しのガード条件が短く書ける!」というコードゴルフの基本テクの1つというわけですw
D: そんなやりたい放題な言語だったのか
B: 混乱しそうだけど、慣れたら使いやすいのかな?
まぁ、あくまでネタなのでそこんところはご承知おきを。
F: F#だと let (>) a b = a % b = 0 みたいに書いて演算子定義できますね
C: a>b だとネタだけど、演算子を関数のように定義できるのは便利かもしれない
F: DSLでも役に立ちますね演算子定義
ああそうですね(想定してなかった反応だ)、DSL への使い途は大いにありそうですね。
B: 質問:
a><><><//.<-~~|b = xxx
みたいに任意の記号列を演算子として使えるんですか?
いえ、Julia は演算子として使用できる記号文字(の組合せ)が規定されています(正確には parser レベルで決まっている)。あと「その記号が単項演算子として利用できるか」「二項演算子として利用できるか」および「演算子の優先順位」も規定されています。それ以外の使い方や「任意の記号(の組合せ)を任意の演算子優先順位で定義する」ことはできません。
その代わり、Unicode に存在するかなり多くの記号が「演算子として利用できる記号文字」として予約されているので、それらを使って新しい演算子を(新しい関数として)定義することはできます(詳細は割愛します)。
B: なるほど。C++の演算子宣言ぽいですね。
記事化における補足
先ほど
きちんとした演算子オーバーロードの仕方は別にある
と書きました(LT中は省いた)が、その方法を簡単に説明しておきます。
例えば「(文字列)/(整数)
で『文字列をホワイトスペースで最大(整数)個に分割する』」という演算子オーバーロードは以下のようになります。
import Base.:/
s::AbstractString/limit::Integer = split(s; limit)
# または
function Base.:/(s::AbstractString, limit::Integer)
split(s; limit)
end
2つの例を示しましたが、本質的には同じで「その演算子を関数として多重定義する(=メソッド追加する)」ということになります。
その際、適用したい関数(演算子)にモジュールプレフィックスを付けるか、import Base.:/
のようにして「どのモジュールに含まれている演算子(をオーバーロードしたい)か」を明示することが重要。これがないと「そのモジュールの演算子の存在を無視して現在のモジュール(地の文なら Main
モジュール)に同じ名前の演算子を定義してしまう(シャドウイング)、という訳です.
あと先ほどは「《演算式》=◯◯ で演算が定義できる」とだけ言いましたが、演算子は関数なので後者の例のようにすれば関数定義の書式でも演算が定義できます。こちらもぜひ覚えてください。
最後に一応動作例も示しておきます:
s = "A B C D E"
s / 3
#> ["A", "B", "C D E"]
3 / 2 # シャドウイングではなくオーバーロードなので本来の数値演算も有効なまま
#> 1.5
③ x.|>[f,g,h]
続いてこれ。ヒントとして「|>
」は パイプライン演算子(パイプ演算子 とも)と言って、…。
F:
[f(x), g(x), h(x)]
F: こういう書き方F#でもよく使うので
さすが F# 使用者、一発正解瞬殺ですね!
③ x.|>[f,g,h]
:複数の関数へのブロードキャスティング!
取り敢えずコード例から。
# 普通のブロードキャスティング
sin.([1, 2, π/4])
#> [0.841471, 0.909297, 0.707107]
# 関数適用のブロードキャスティング
π/4 .|> [sin, cos, tan]
#> [0.707107, 0.707107, 1.0]
# 複数vs複数の例
[1 2 π/4] .|> [sin, cos, tan]
#> [0.841471 0.909297 0.707107
#> 0.540302 -0.416147 0.707107
#> 1.55741 -2.18504 1.0 ]
えーとまず、Julia の ブロードキャスティング について改めて説明します。実はこれまでにも既に何度か出てきてしまっていますが。
Julia では関数や演算子に .
を付けることで簡単に ベクタ化(1つの値を受け取る関数を多値に拡大)できます。その時次元数や要素数が合わなければ一定の法則で次元拡張も実施されます。これがブロードキャスティングです。
例えば sin()
という関数がありますが、sin.([1, 2, π/4])
と書くとこの sin()
という関数を 1
, 2
, π/4
それぞれに適用した結果の配列が帰ってきます。「1つの関数に複数の値を適用させる」というブロードキャスティングの基本です。
続いてパイプライン演算子は、例えば x |> f
と書くと f(x)
という関数呼び出しと同じになります。
これをブロードキャスティングと組み合わせることで、「1つの値を複数の関数に適用させる」ということが可能になります。それが x .|> [f, g, h]
という書き方なのです。F: の答えたとおりこれは [f(x), g(x), h(x)]
と同じ意味になります。
なお「複数の値を複数の関数に適用」することもできます(3つめの例)。
B: 便利そう。
でこれが何の役に立つかというと(次のコード例)
".|>[show,print]".|>[show,print]
## ".|>[show,print]".|>[show,print]
これ。Julia で現在見つかっている最短の Quine です。
Quine とは(ここでは)『ソースコードと全く同一の出力をするプログラムのこと』です(ただしソースコードを読み込んで出力するコードは反則)。この Quine はコードゴルフコンペサイトで Julia では同率1位のコード(32bytes/32chars)です。
解説。Julia で "a"|>show
というコードは "a"
と出力されます。"a"|>print
は a
と出力されます。組み合わせると、"a".|>[show,print]
は "a"a
と出力されます。なので a
の部分を .|>[show,print]
に置き換えた ".|>[show,print]".|>[show,print]
は ".|>[show,print]".|>[show,print]
と出力される、というわけです!
D: はぁー綺麗なquine
ですよね! この Quine めっちゃ好きです!
④ A'B
次はこれ。これは見覚えのある方も多いかも?
C: これは前に聞いたことがある気がする
C: A の転置と B の行列積
E: ぱっと見行列を転置して掛けてる
これはやはり正解者多いですね。
④ A'B
:行列 A
の転置と B
の行列積
A'B
は A' * B
と同じ意味になります。A'
は行列 A
の転置です。
B: lisp的には
A
と'B
に見えるけど違うんだ。
あー、Lisp だとそうですね、'B
が、シンボルでしたっけ?quoteでしたっけ?(quoteが正解でした)そうではなくて A'
と B
です。
コード例を見てください:
v = [1, 2, 3];
[100, 10, 1]'v
#> 123 # 各桁の数字から10進数値を構築
v'v
#> 14 # 内積
@.v'v # == v'.*v
#> 3×3 Matrix{Int64}:
#> 1 2 3
#> 2 4 6
#> 3 6 9
先ほど「行列の~」と説明しましたが、ベクトルに対しても有効です。例えば v = [1, 2, 3]
という3次元ベクトルに対して v'v
と書くと、数式で言うところの(列ベクトル $v$ に対する)$v^\top v$ と同じ意味になり、つまりベクトルの 内積 が計算できます(v'v == 14
、結果はスカラーになる)。
B: これはきれいですね。
あと v'v
は v' * v
と同じ意味になりますが、ブロードキャスティングと組み合わせて @.v'v
と書くと v' .* v
と同じ意味になって、掛け算の九九表のような掛け算Matrixが得られます。
これを利用したのが次のコード例:
R=2:99;@.R[R∉[R'R]]|>println
## 2
## 3
## 5
## 7
## :《中略》
## 83
## 89
## 97
はい 素数列挙 です。2桁までの素数列挙ならコードゴルフでこれが最短(30bytes/28chars)です。(@.
が前方にあっての)R'R
の部分が掛け算Matrixを形成していて、『R
(=2:99
)のうちこのMatrixに入っていない要素が素数』というわけですね。
E: \notinも使える!?
あ、はいそうです、∉
演算子は Julia 標準で用意されています、x∉A
は !(x in A)
と同じ意味になります。
B:
@.
の位置がそこなんだと思った。
あー、そうですね、素数を取得するだけなら例えば R[@.R∉[R'R]]
と書いても同じです。これだと @.
の有効範囲がその外側の [~]
の中に限定されます。なので R[@.R∉[R'R]]|>println
だと [2, 3, 5, 7, …, 97]
と出力されます。コードゴルフのレギュレーションとして「1行に1つずつ出力」というルールがあったために @.R[R∉[R'R]]|>println
と書いています、これだと @.
の有効範囲は行末までになるので(println.(R[R.∉[R'.*R]])
と同じ意味になる)。
なお補足として、これはあくまである程度小さい数までの素数列挙だから現実的なだけで、3桁くらいまではスピードもメモリ使用量もコードゴルフ的な短さも有効ですが、ある程度大きな桁数の素数まで列挙しようとすると O(N²)
なので決して実用的ではありません(コードゴルフ的にもタイムアウト(大抵5秒制限TLE)になってしまう)。
⑤ A⊆B⊆A
先ほど ∉
という集合の演算記号(\notin
)が出てきました。こちらも集合の包含記号ですね。てそれ言ったらほぼ答え言ってるようなものですね。
E: (集合としての)A==B
C: python でいうとset(A) == set(B)
かな
もうほぼ正解出ていますね。
⑤ A⊆B⊆A
:集合としての A
と B
の等価性
Julia には issetequal()
という関数が用意されています。これは『引数に渡した2つのコレクションが集合として等しい(ユニークな要素の組合せが一致する)かどうか』を返す関数です。
(補足:C: さんの言うとおり、Julia でも Set(A) == Set(B)
と書くこともできます)
A = 1:3;
B = [3,1,2,1];
C = [3,1,4];
D = [3,1,2,4];
A⊆B⊆A
#> true
A⊆C⊆A
#> false
A⊆D⊆A
#> false
例えば↑のコード例で、issetequal(A, B) === true
です(Set(A) == Set(B)
も言えます)。
ところで数学的には、2つの集合 A
と B
が A⊆B
かつ B⊆A
なら、2つは集合として等しい、となりますよね。それが直感的に表せているわけです。実際上の例で、A
とC
、A
とD
は A⊆C
C⊆A
D⊆A
がそれぞれ成り立たないので集合として異なるもの、になりますよね。
G: そんな(半角)記号あるんですか?知らなかった
G: 記号があっても、キーボードから打てない...
あー、普通は打てないですよね。でも Julia はこういう数学記号を入力できる機能も提供してたりします、LTじゃ全然時間が足りないので省略しますw(註:後述)
それよりも、issetequal(A, B)
よりも(Set(A)==Set(B)
よりも)A⊆B⊆A
の方が、断然短いですよね!
はい、これもコードゴルフ以外の有用な使い途は(たぶん)ありません!
こんなコード例を挙げてみます↓
A>R=R⊆A≠println(join(A,-)),R.|>x->A<A∪x>R
[]>1:4
## 1-2-3-4
## 1-2-4-3
## 1-3-2-4
## 1-3-4-2
## :《中略》
## 4-2-1-3
## 4-2-3-1
## 4-3-1-2
## 4-3-2-1
これは 順列列挙 のショートコードです。A⊆R
は常に成り立つので R⊆A
とだけ書いている点にも注目。これが成り立ったときだけ println(~)
が実行される仕組みです。
その他色んなテクニックを駆使しているので、腕に覚えがあればぜひ解読してみてくださいw
D: まずどこが結合しているのか全然わからないw
ぱっと見一番分かりにくいかも?ってところは、後半の A<A∪x
の部分かな? と思います。なので…。
⑥ A<A∪x
その部分だけ追加で解説しましょう。まずこのコードがどんな意味だと思いますか?
これも集合演算が絡んでいることから正解を導き出せるかも。
E: x \in A
C: x not in A
真っ二つに分かれたw 正解は…。
⑥ A<A∪x
:x∉A
(!in(x,A)
)
C: の方が正解です!
ただし、A
は Julia の集合型(Set
)ではなく配列(Array
)などとします。
まずはコード例を:
A = 1:3;
all(A∪x==A for x in 1:3)
#> true
all(A∪x==[A;x] for x in 4:10)
#> true
all(A<A∪x for x in 4:10)
#> true
∪
はご想像の通り 和集合演算子(\cup
)です。この時、左被演算子が配列(の類)、右がスカラーなら、新しい配列が返ってきますが、x
がすでに A
の要素として含まれていたら(x∈A
)、A∪x==A
となります。
これ、オブジェクトとしての同一性ではなく、配列としての同一性です。配列は大小の比較演算子で比較できますが、その比較方法は 辞書式順序比較 です、つまり要素を頭から順に比較して「同じ位置の要素の大きい方の配列が大きい」「途中まで同じなら長さの長い方が大きい」「それで区別できない(=最初から最後まで要素が同じ)の場合は『等しい』」ということです(文字列の一般的な大小比較と同様ですね)。
で。x
が A
の要素として含まれていなかったら、A∪x
は A
の要素に x
を追加した配列([A;x]
と同等(注:これは配列の結合の書式です))となります。A
より後ろに要素が多く付いている分、大小比較でA∪x
の方が大きくなる。つまり A<A∪x
というわけです!
で。でもそれだと「x∉A
の方が短くね?」と思うかも知れません。でも使い途はあります、そう、コードゴルフならw
ということで先ほどのコード例を再掲:
A>R=R⊆A≠println(join(A,-)),R.|>x->A<A∪x>R
[]>1:4
## 1-2-3-4
## 1-2-4-3
## 1-3-2-4
## 1-3-4-2
## :《中略》
## 4-2-1-3
## 4-2-3-1
## 4-3-1-2
## 4-3-2-1
この問題部分 A<A∪x>R
は、A<A∪x && A∪x>R
と同じ意味であり、「A<A∪x
が成立したときに限り A∪x>R
を実行する(再帰呼び出しする)」という挙動になります。これを本来の意味で書き直すと x∉A && A∪x>R
ということになるわけですが。
空白を除去した x∉A&&A∪x>R
と A<A∪x>R
、どちらが短いですか? 一目瞭然ですねw
B: ガードやったんか・・・。
記事化における補足
先ほど「そんな記号があっても、キーボードから打てない」「入力できる方法はありますが省略します」と言っていましたが、軽く解説しておきます。
Julia の REPL には、特殊記号等を手軽に入力できる方法が提供されています。
例えば ⊆
(集合の包含記号、部分集合)は REPL 上で \subseteq
+Tab で入力できます(Tab補完の一種)。
あと Julia に対応したエディタ/IDE、またはそれらの Julia 用プラグインには、それと類似の機能を提供するものも多いです(\subseteq
などと入力すると特殊文字の候補が表示されて、選択して Enter で確定(スニペット機能のようなもの)を提供したり等)。
本記事で紹介した数学記号を中心に、いくつかの文字とその入力方法を↓に示しておきます(これ毎回どこかで書いてる気がする…)
あとこれ見て分かる方には分かるとおり、入力方法の多くは TeX のコマンドが由来になっています。
文字 | 入力方法 | 説明・備考 |
---|---|---|
∈ |
\in |
包含演算子(含む) |
∉ |
\notin |
包含演算子(含まない) |
⊆ |
\subseteq |
包含演算子(部分集合) |
⊊ |
\subsetneq |
包含演算子(真部分集合) |
∪ |
\cup |
和集合演算子 |
∩ |
\cap |
積集合演算子 |
÷ |
\div |
整数除算演算子(7÷2==3 ) |
π |
\pi |
円周率(数学定数) |
≥ |
\ge |
比較演算子(以上、>= のエイリアス) |
𝕏 |
\bbX |
旧Twitter |
まとめ
- Julia でコードゴルフも意外とパズル要素が多くて楽しいよ!
- Julia 楽しいよ!
参考文献・リンク等
-
Code Golf (コードゴルフコンペサイト)
- Wiki: Julia (JuliaのコードゴルフテクTips)
- Tips for golfing in Julia (コードゴルフTips、ただしバージョンが古かったり現在通用しないテクニックもしばしばあるので注意)