do 記法
無名関数を使った様々なタスクを書きやすくする記法が用意されている。基本的には
第一引数を除いた関数呼び出し do 引数リスト
関数定義ボディ
end
と書くと引数リストと関数定義から無名関数を作って、1行目の関数の第一引数に渡してくれる、というもの。例えば整数のリスト中の値をすべてインクリメントするには、map
を使うと次のようになる。
map(x -> x + 1, [1,2,3])
do 記法を用いると次のようになる。
map([1,2,3]) do x
x = x + 1
end
このサンプルだとなんにも嬉しくない。。しかしこの構文を用いると、Javaのtry-with-resourceのようなことが書ける。何かを行う前にlock
し、行った後でunlock
するには、
function with_lock(f, args...)
lock(args...)
try
f()
finally
unlock(args...)
end
end
のように定義しておいて、
withlock(args...) do
BODY
end
のように呼び出せば良い。Juliaではsmalltalk やRubyのようにコードブロックをファーストオーダオブジェクトにして扱うことは(たぶん)できないのだけど、この記法では無名関数という形でコードブロックを切り出せるので、いろいろ便利な使い方がありそう。
|> 演算子
似たような関数と引数の入れ替えを、パイプ演算子でも書くことができる。
引数 |> 関数
は
関数(引数)
と等価だ。関数がひとつだとあまり意味があるようには思えないが、いくつもの関数を連鎖させる場合に、関数呼び出しのネストで書くよりはパイプでつないだ方が見通しがいい。
例えば、1から10までの数字を2倍して合計する、というのは次のように書ける。
julia> [1:10;] |> x->2x |>sum
110
ドット記法
Pythonのnumpyでは配列に対する一括演算がいろいろと整備されている。例えば、配列の全てに対して同じスカラーとの演算を行う事ができる。下のコードは1000, 100, 100 のすべてがゼロの配列を作り、その全てに1を加えている。
import numpy as np
np.zeros((1000, 100, 100)) + 1
Juliaではこの機能が一般化されていて、任意の関数による配列に対する一括操作を容易に記述できる。それがこのドット記法だ。関数名の後ろにドット(ピリオド).
をつけると、その関数を配列に対して一括して適用するようになる。
# 1を足すだけの関数
julia> addone(x::Float64) = x + 1.0
# スカラに対して実行
julia> addone(2.0)
3.0
# 配列に対して実行
julia> addone.(zeros((1000,100,100)))
この記法はbroadcast
という組み込みメソッドを呼び出しているのと等価だ。
julia> broadcast(addone, zeros((1000,100,100)))
numpy のvectorize
pythonのnumpyでも実は同じようなことがvectorize
というnumpy関数を用いるとできる。この関数は、スカラを対象とした関数を引数として、配列を対象とする関数を返す。
> vaddone = np.vectorize(lambda x: x + 1)
> vaddone(np.zeros((100,100,100)))
ただ、vectorize
で作った関数はめちゃめちゃ遅い。numpyの組み込み関数と比較してみよう。次のような経時関数を用意して計測する。
def timer(f):
before = time.time()
f()
print(time.time() - before)
> timer(lambda : np.zeros((1000,100,100)) + 1.0)
0.02180790901184082
> timer(lambda : vaddone(np.zeros((1000,100,100))))
2.071074962615967
二桁違う。これは、numpyの組み込み関数はCで書かれているのに対して、vectorizeで作った関数はPythonレイヤでループを回すためだ。
Juliaのbroadcastの速度
Juliaの方でも同じようなことをしてみよう。まずは組み込みの配列処理関数.+
を使う。@time
は 時間計測をしてくれる。便利。JuliaはJITコンパイルするので1度目はちょっと遅い。下に示したのは2回め
julia> @time .+(zeros((1000,100,100)), 1)
0.035817 seconds (11 allocations: 152.588 MiB, 38.95% gc time)
次にドット記法による実行。
julia> @time addone.(zeros((1000,100,100)))
0.037969 seconds (11 allocations: 152.588 MiB, 35.68% gc time)
Juliaの場合これら2つの結果はほとんど変わらない。こうしてみるとnumpyのネイティブがやはり速くてすごいのだけど、ユーザが定義した関数を実行するならJuliaの方が高速になりそう。
@. マクロ
いちいち.
をつけて回るのは面倒だ、ということで、@.
というマクロが用意されている。後続の式のすべての関数呼び出し、演算子に.
が自動的につく。
julia> a = zeros((1000,100,100))
julia> @. addone(a)
julia> @. a + 1.0
こうすると、見通しがいい。
関数合成
複数の関数を複合した合成関数を、演算子∘
で作成することができる。この∘
は\circ + tab
で入力できる。f
とg
をそれぞれ1入力の関数とすると、(f∘g)(x)
は、f(g(x))
に等しい。
julia> map(x->f(g(x)), [1,2,3])
と書くのではなく、下のように書ける。2つぐらいだとあまりありがたみがない?
julia> map(f∘g, [1,2,3])