JuliaのFunction-like objectについて理解したい

The code describing the example of the function-like object in Jullia

どうにもとっつきにくかった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はメソッドの定義をするfunctionendのスコープでのみ用いられる変数を意味します。この定義内では「p::Polynomialxを使って値を返す処理」を書くことができます。上の例では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を具体的に使っていきます。

今までと異なり、この式では変数pPolynomialのオブジェクトを代入しています。例にはありませんが、ここで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.CallableUnion{Function, Type}でしかないため)

Function-like objectをいつ使うべきか

基本的にはパラメータを持つ関数を作るときには便利な気がします。
もちろん、ただ関数の値を計算するだけなら

function gaussian(x; mu=0, sigma=1)
    # 適当な処理
end

のように、オプション引数にパラメータを持たせた関数を作ってもいいのですが、関数自体を対象として様々な計算を行う場合(例:関数空間での内積など)を考えるときには関数そのものをcomposite typeとして扱った方が何かと便利です。

私が初めてfunction-like objectの使用例を見たのはDiffEqNoiseProcess.jlOrnstein-Uhlenbeck過程の定義の部分でした。今回のサンプルコードの多項式のようにcomposite typeではパラメータのみを保持させ、実際の確率過程のパス生成はfunction-like objectとして関連付けられたメソッドで処理しています。

完璧に使いこなすにはまだまだハードルの高さを感じてしまいますが、より良いコードのためにも、使えそうなところでは躊躇わずに使えるようになりたいものです。

JuliaのFunction-like objectについて理解したい” への2件のフィードバック

  1. 一つの有用な(と私が思う)使い途は、クロージャの代わり。
    以前にQiitaで「Julia で Closure のパフォーマンスを気にしてみる」( https://qiita.com/antimon2/items/8d464d54cf2da6239635 )という記事を書きましたが、よくある言語でクロージャで実現しがちなものをFunction-like Objectで実装すると、パフォーマンスがけっこう上がります。
    これ内部的にも型安定性に繋がっている(上記の記事の主張)というのもありますし、それを使用する状況でも型安定性に繋がりますね。ただのクロージャだと毎回オリジナルな Function の subtype になって安定しないのに対して Function-like Object なら常に「その型(この記事の例だと Polynomial 型)」になりますから。

  2. 確かに「クロージャの代わり」という使い方は便利そうです!
    ご紹介の記事中の例のように、1つでもAnyを潰そうとするならFunction-like objectは有力な手段になりそうです。

    一方で、変数の隠蔽となるとやはりクロージャが必要ですね。
    Function-like objectでもsetproperty!(obj, symbol)やgetproperty(obj, symbol)を定義することで、フィールドの明示的な隠蔽はできますが、クロージャを使った方ががスマートな気がします。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です