JuliaのFunction-like objectについて理解したい
どうにもとっつきにくかったJuliaの"Function-like object"を公式ドキュメントを読んで(たぶん)理解したのでメモ。
以下のコードはすべてJulia v1.4.2で確認しています。
Function-like objectの定義
"Function-like object"は、ざっくり言うと「Function型でなくても関数のように振る舞うオブジェクト」です。
もう少し細かく言えば「Function型,Type型以外でメソッドを関連付けられたオブジェクト」という認識でいい…はずです。
Juliaでは関数は「引数のタプルを返り値にマッピングするオブジェクト」、メソッドは「関数がとるかもしれない動作定義」と定義されています。Juliaでは1つの関数に対し、引数の違いによっていくつものメソッドが関連付けられているのはご存知のことと思います。
今回は公式ドキュメントに載っているサンプルコードを用いて説明していきます。
julia> struct Polynomial{R}
coeffs::Vector{R}
end
julia> function (p::Polynomial)(x)
v = p.coeffs[end]
for i = (length(p.coeffs)-1):-1:1
v = v*x + p.coeffs[i]
end
return v
end
julia> (p::Polynomial)() = p(5)
julia> p = Polynomial([1,10,100])
Polynomial{Int64}([1, 10, 100])
julia> p(3)
931
julia> p()
2551
1つずつ説明していきたいと思います。
struct Polynomial{R}
coeffs::Vector{R}
end
まずComposite type(複合型)を定義します。
コードの気持ちとしては「多項式」という型を作っておいて、その多項式の係数coeffs
を持たせておきたいということです。
例えば、Polynomial([1,2,3])
なら\(1+2x+3x^2\)を表現したいわけです。
function (p::Polynomial)(x)
v = p.coeffs[end]
for i = (length(p.coeffs)-1):-1:1
v = v*x + p.coeffs[i]
end
return v
end
早速関数定義っぽいものが出てきましたが、これがオブジェクトへのメソッドの定義です。function-like objectの要になります。
これによってPolynomial
のオブジェクトを関数のように扱うことができるようになります。
普通のFunction
型の関数定義と異なる点は
- 関数名に型指定
::Polynomial
がついていること p
という名前の関数が定義されるわけではないこと
の2点です。特に後者は紛らわしいので注意してください。
このp
はメソッドの定義をするfunction
〜end
のスコープでのみ用いられる変数を意味します。この定義内では「p::Polynomial
とx
を使って値を返す処理」を書くことができます。上の例ではp.coeffs
を使って多項式の具体的な値を計算する処理を書いています。例えば、Polynomial([1,2,3])(2)
であれば9
が返ってきます。
(p::Polynomial)() = p(5)
これも1つ上と同じくPolynomial
オブジェクトに対するメソッドの定義になりますが、上と違うのは引数x
が無いことです。
ここでも左辺のp
はこのメソッド定義内でのみ使用される変数で、Polynomial
のオブジェクトを意味します。
右辺のp(5)
はPolynomial
に既に関連付けられたメソッド(p::Polynomial)(x)
を引数5
で呼び出しています。
p = Polynomial([1,10,100])
ここから(ようやく)上で定義したfunction-like objectを具体的に使っていきます。
今までと異なり、この式では変数p
にPolynomial
のオブジェクトを代入しています。例にはありませんが、ここでmethods(p)
を呼び出して関連付けられているメソッドを確認してみましょう。
julia> methods(p)
# 2 methods:
[1] (p::Polynomial)() in Main at REPL[3]:1
[2] (p::Polynomial)(x) in Main at REPL[2]:2
上で定義したメソッドがPolynomial
オブジェクトに関連付けられているのが確認できました。
p(3)
p()
最後の2つはまとめて説明します。Polynomial
オブジェクトが代入されたp
を関数のように扱っています。
ここで呼び出されているメソッドは上で確認したとおりです。
以上がfunction-like objectのサンプルコードの説明になります。
個人的にはこのサンプルコードだとメソッドの定義内で使うp
と、具体的に代入される変数p
が別物なのに同じ文字を使っているので混乱を招きそうな気がしました。
(せめて後者はpoly
とかにしてあげればいいのに…)
ちなみに関連付けるメソッド内でそのオブジェクトのフィールドを使わないような場合は
function (::Polynomial)()
return 0
end
のように定義内で使うp
を頭に書かなくても大丈夫です。
Function-like objectかどうかの判定
あるオブジェクトがfunction-like objectかどうかの判定は以下のように関数callable()を定義してあげると便利です。
julia> callable(x) = !(isempty ∘ methods)(x)
callable (generic function with 1 method)
julia> callable(p) # pは上のサンプルコードのPolynomial
true
julia> struct Foo end
julia> foo = Foo()
Foo()
julia> callable(foo) # Fooのオブジェクトはfunction-like objectではないのでfalse
false
ただし、上の関数はfunction-like objectだけでなくFunctionやType(コンストラクタもメソッドなので)でもtrue
を返します。
julia> a = 100
100
julia> callable(a) # a::Intはfunction-like objectではない
false
julia> callable(Int) # Type{Int}はコンストラクタとなるのでtrueとなる
true
ちなみにx isa Base.Callable
ではfunction-like objectは拾えないので注意です。(Base.Callable
がUnion{Function, Type}
でしかないため)
Function-like objectをいつ使うべきか
基本的にはパラメータを持つ関数を作るときには便利な気がします。
もちろん、ただ関数の値を計算するだけなら
function gaussian(x; mu=0, sigma=1)
# 適当な処理
end
のように、オプション引数にパラメータを持たせた関数を作ってもいいのですが、関数自体を対象として様々な計算を行う場合(例:関数空間での内積など)を考えるときには関数そのものをcomposite typeとして扱った方が何かと便利です。
私が初めてfunction-like objectの使用例を見たのはDiffEqNoiseProcess.jlのOrnstein-Uhlenbeck過程の定義の部分でした。今回のサンプルコードの多項式のようにcomposite typeではパラメータのみを保持させ、実際の確率過程のパス生成はfunction-like objectとして関連付けられたメソッドで処理しています。
完璧に使いこなすにはまだまだハードルの高さを感じてしまいますが、より良いコードのためにも、使えそうなところでは躊躇わずに使えるようになりたいものです。