関数オブジェクト
関数オブジェクト(かんすうオブジェクト、function object)は、プログラミング言語において、関数(サブルーチンないしプロシージャ)を、オブジェクトとしたものである。。なお、ここでのオブジェクトの語は、いわゆるオブジェクト指向のそれに限らず、「第一級オブジェクト」という語におけるのと同じ、メモリ上に領域を確保されたもの、といった意味である。関数が第一級オブジェクトである場合は特に第一級関数と言う。
、構文の設計によっては、y = f(x) といったような、通常のサブルーチン呼び出しと全く同じ構文で、関数オブジェクトが保持しているサブルーチンを呼び出せる言語もある。一方、通常のサブルーチンのように呼び出すことはできず、applyといった特別な名前のメソッドを経由して呼び出す必要のある言語もある。
また、変数束縛が閉じられた関数オブジェクトはクロージャである。C#などの.NET言語には関数オブジェクトのようなものとして、オブジェクトのインスタンスとその振る舞いであるメソッドとを結びつけて管理することのできる、デリゲートがある。無名関数も参照。
関数オブジェクトの典型的な用途は、より優れたコールバックを記述することである。C言語では、コールバックには関数へのポインタを使う他ないが、コールバックの内外で状態変数を共有できない。この制限のために、関数の動的な振る舞いやインターフェイス設計が制約されてしまう。以下に例を示す。
コールバック関数内で外部の状態変数を参照するためには、グローバル変数を利用したり、構造体メンバーに状態変数へのポインタを持たせたうえでコールバック関数の引数を経由して状態変数のアドレスを渡したり、といった工夫が必要になる。
関数オブジェクトは、上記のように煩雑になりがちな設計パターンを言語機能として組み込み、関数を可搬性の高いオブジェクトとして容易に利用できるようにするものである。
LISPや、Smalltalk、C++、Java、C#、Perl、Python、Rubyなどの現代的なオブジェクト指向言語は、ほとんどが関数オブジェクト、あるいは同等の機能をサポートしており、。
関数オブジェクトは、LISPにおいてその初期から研究された。計算機プログラムの構造と解釈の第3章でも解説されている。
オブジェクト指向言語では、Smalltalk において、ブロックが関数オブジェクトの記法となるよう設計された。たとえば配列の各要素を通常の大小関係とは違う順序で並べ替えたい場合、比較のための関数オブジェクトを引数に取るソートメソッドを、引数としてブロックを付けて、呼び出す。ブロック内には、カスタマイズ版の比較手続きを記述する。ソートメソッド内での比較は、渡された関数オブジェクトの手続きを呼び出すことで行なわれ、期待する大小関係でのソートが行なわれる。これは、Strategyデザインパターンの完全な具現化であり、着脱可能 (pluggable) な振る舞いを促進するものである。
C++では、クラスあるいは構造体において関数呼び出し演算子の多重定義が可能となっている。具体的には、任意の引数を持つoperator()メンバー関数を定義することで、そのオブジェクトのインスタンスを指す変数の名前が、あたかも関数名(関数指示子)であるかのような構文で、定義した関数を呼ぶことができる。このような C++ のオブジェクトを、C++ の用語ではファンクタ (functor) と呼ぶ(#C++のファンクタとファンクショノイド を参照)。
C++のclass/structは、デフォルトのアクセスレベル(アクセス修飾子)がprivate/publicであるという違いしかないため、関数オブジェクトの記述ではclassの代わりにstructが使われることも多い。
配列の中から特定の条件に該当する要素の個数を数えるルーチンの例を考えてみよう。ただし条件の判定処理は自由にカスタマイズできるものとする。
関数へのポインタを使用する C のプログラムは、たとえば下記のようになる。
一方、C++の関数オブジェクトを利用すると下記のようになる。
コールバックを述語 (predicate) として countIf() 関数テンプレートに渡す際、関数へのポインタではなく構造体のインスタンス(関数オブジェクト)を渡していることに注意されたい。
上記の例において関数オブジェクトによるカスタマイズを可能にしているのは、テンプレートと関数呼び出し演算子のオーバーロードによる静的ダック・タイピングである。関数テンプレートを実体化する際の字句解析に適合しさえすれば、述語にはどんなオブジェクトでも渡せる。
コールバック関数が実行されると、他のメンバー関数と同様に働き、すなわちオブジェクトの他のメンバー(データや関数)に対して完全にアクセスすることができる。
C++ の Standard Template Library (STL) では、テンプレートの述語 (predicate) として関数オブジェクトを多用している。
以上のように C++ では、関数オブジェクトは、「関数呼び出しと同じ構文で、メンバ関数を呼ぶことができるオブジェクト」として実装されている。これを C++ の用語ではファンクタと呼んでいるが、これはStandard MLのfunctorや、数学における関手とは関係ない(と考えたほうが良い)。
C++ では、主要なメソッド一つを持つオブジェクトを「ファンクショノイド」と言い、その一種で、その主要なメソッドが operator() であるオブジェクトが「ファンクタ」である、と説明される。これは C++ の用語であり、C++ を離れた文脈では、関数オブジェクトすなわちファンクタ、ではないので注意。
C++ における関数オブジェクトの利点のひとつは、関数ポインタと異なり、インライン化できるためパフォーマンスが良い点である。たとえば、引数をインクリメントさせるシンプルな関数は関数オブジェクトとして実装できる:
通常の関数:
STL 関数 std::for_each() を用いると下記のようになる:
ここに std::for_each() を適用すると下記のようになる:
いずれの for_each()も期待通りの動作をするが、最初の方法では以下のように展開される。
二番目の方法では以下のように展開される。
for_each<int*, IncrementFunctor>()の場合には関数が既知であるためコンパイラがインライン化できるが、for_each<int*, void(*)(int&)>() の場合にはコンパイル時に関数が不定でありインライン化できない。
現実には、コンパイラに指示すれば簡単に関数を既知にすることができる。コンパイラが関数の定義を認識しており、それがクラスの内外いずれでも同じように行われていさえすればよい。インライン化しない場合、リンカは関数がクラスの関数だとの指示さえあれば同じ関数の別のコンパイル単位の複数回の定義をエラーを生成せずに黙って見過ごす。リンカは同じ関数の定義がクラスの関数でない場合には複数回の定義を許容しないためである。
もうひとつの例として、std::sort()関数テンプレートを挙げる。
std::qsort()およびstd::sort()の実装はいずれもクイックソートとは限らず、処理系依存のため、直接性能を比較することに意味はないが、通例インライン展開が利用できるstd::sort()のほうが性能が良くなる。また、述語に関数ポインタを利用するよりも、関数オブジェクトを利用したほうが性能が良くなる。C++11で追加されたラムダ式は、コンパイラによって内部的には関数オブジェクトの定義に展開されるため、通例関数オブジェクトを利用した場合と同等の性能になる。
関数オブジェクトの利点の一つは、関数の呼び出しをまたいで状態を(オブジェクトのフィールドとして)保持できる点である。たとえば、下記のコードは10以上の数を数えるジェネレータ(引数をとらない関数)を定義し、11 回呼び出し結果を出力している。
関数オブジェクトで状態を保持する場合、明示的にメンバー変数を定義したうえで、関数オブジェクトのメンバーとして操作を記述しなければならなかった。C++11で導入されたラムダ式では、外部の変数のキャプチャをサポートし、クロージャを実現することができる。ラムダ式によってキャプチャされた変数は、コンパイラによって暗黙的に生成される関数オブジェクトのメンバー変数となる。
D言語には関数オブジェクトとして、デリゲートとクロージャの両方がある。
D言語におけるデリゲートとクロージャの違いは、コントロールが変数のスコープから一旦抜けても、変数の寿命が続いているか、そうでないかである。コンパイラにより保守的に、自動的に決定される(後から変数を参照する可能性があればクロージャとする)。
D言語は、関数リテラルやラムダ式もサポートしている。
コンパイラがインライン化できるようにするため(上記参照)、関数オブジェクトをC++形式の演算子のオーバーロードを用いて宣言することもできる。しかし、D言語ではテンプレート引数として関数等を渡す手法が一般的である。
Javaでは関数が第一級オブジェクトでないため、関数オブジェクトの代わりとして、一つのメソッドを持つインタフェースが使われる。代表的なインタフェースとしては、run()メソッドを持つjava.lang.Runnableが挙げられる。実際のコーディングにおいては、そのようなインタフェースを実装するクラスの定義には、内部クラス (inner class) や、メソッド内の変数をキャプチャしてクロージャのように振る舞うことのできるローカルクラス (local class) あるいは匿名クラス (anonymous class) がしばしば使われる。
Javaの標準ライブラリの例では、java.util.Collections.sort() はリストと、意味的には関数を引数にとる。この関数はリスト内のオブジェクトを比較する役割を持つ。しかし、Javaは関数が第一級オブジェクトでないため、java.util.Comparatorインタフェースを実装したオブジェクトを渡す。下記の例のように使用する:
Java 8からは匿名クラスの糖衣構文としてラムダ式をサポートする。ラムダ式を利用すると、以下のように簡潔に記述することができる。
また、Java 8はメソッド参照もサポートするようになった。メソッド参照は、内部的にはインタフェースを利用して実装されている。
Pythonでは、関数は、文字列や数値、リストなどをはじめとする他の任意のデータと同様のオブジェクトである。つまり、Pythonは第一級関数を扱うことができる言語である。また、__call__() メソッドを持つ任意のオブジェクトを関数呼び出しの構文で呼び出すことができる。
例として、Accumulator クラス (ポール・グレアムのプログラミング言語の文法と明快さの研究に登場する) を挙げる。
下記のように使用する(対話的インタプリタを用いている):
Python で関数オブジェクトを定義するもう一つの方法として、ネストした関数定義、といった形の構文を使う方法がある。
一方で Python には、その内部に式しか書くことができない(文が書けない)という強い制限のある lambda form しか関数リテラルに相当するものがなく、手続き的なものは名前を付けて定義しなければならない。これはそうするのが良いプラクティスとされているからである。
Lisp においても、関数は、文字列やベクトル・リスト・数値と同様に変数に入れたり関数から返したりできる第一級オブジェクトであり、第一級関数を扱うことができる言語である。
Lisp はその最初から (lambda ...) という特殊形式による関数リテラルをはじめとして、第一級関数を扱うことができる言語であったが、1960年代から1970年前後までの実装では、動的スコープのためクロージャにはなっておらず、funarg問題(Funarg problem)が認識されることとなった。Scheme で、静的スコープと無限エクステント(メモリ上のオブジェクトは参照されている限り生存し続ける)による解決が示され、Common Lisp をはじめとする現代的な Lisp の多くは静的スコープを採用しており、lambda 特殊形式ではクロージャが作られる。しかし Emacs Lisp のように動的スコープの Lisp もまだ広く残っている。
Scheme では変数と関数で名前空間が分かれておらず、変数名を関数名と同様に使ってプログラムを書くことができる。
これを実行すると、
hello, world
hello, Scheme
のように出力される。
これに対し、伝統的な Lisp の多くや Common Lisp は変数と関数で名前空間が分かれている。同様のプログラムを Common Lisp で書いた例を示す。
これを実行すると、
hello, world
hello, Common Lisp
のように出力される。
Common Lisp では、名前に対して、変数としての値と、関数とが別々に結びつけられていて、文脈により(カッコ内の並びの先頭にあるか、そうでないかにより)どちらかがアクセスされる。F という変数に関数値を束縛する所では、通常の文脈において関数にアクセスするために #' を名前の前に付けている。一方、並びの先頭の位置に、この例では F と書いても、その名前の関数がないというエラーになる。Common Lisp では一般に、FUNCALL か APPLY 関数を使って呼び出す。
変数と関数の名前空間を分けるか同じにするか、という議論は、Lisp に限らず現代的なプログラミング言語の設計において話題になる。ML などの現代的な関数型言語では当然のように同じ名前空間である。スクリプティング言語では、Python や JavaScript は同じ名前空間としたが、Ruby ではローカル変数とメソッドで別の名前空間とした。Lisp では「Lisp-1 対 Lisp-2 の議論」などと呼ばれる(Common Lisp#The function namespace を参照)。
Ruby では、便宜上 Object クラスのローカルインスタンスメソッドを「グローバル関数」と呼んでいるといった例外はあるが、関数は存在せず、全てメソッドである。メソッドはオブジェクトではなく、変数とメソッドで名前空間が違う。
Ruby には、メソッドの他に手続きの表現としてブロックがある。Ruby のブロックは、メソッド呼び出しにオプショナルに付加できるもので、暗黙の引数といったような感じで呼び出されるメソッドに渡される。ブロックからクロージャが作られ、呼び出された側からは yield という特殊なグローバル関数により、そのクロージャを呼ぶことができる。
ブロックは直接にはオブジェクトではない。しかし、メソッド定義の仮引数の記述の最後に、&foo のように & を先頭に付けた引数を付けるなどすることで、簡単に Proc オブジェクトとして得ることができる。また、メソッド呼び出しの最後の実引数として、引数の前に & を付けることで、Proc オブジェクトをブロックの代わりに渡すこともできる。Proc オブジェクトの手続きは call というインスタンスメソッドにより呼ぶことができる。
Proc オブジェクトは文脈という環境を持つ関数オブジェクト状のものである。これに対し、レシーバ( foo.bar() のようにメソッドを呼び出す時、foo の指すオブジェクトをレシーバと言う)という環境を持つ関数オブジェクト状のものが Method オブジェクトである。Method オブジェクトはメソッドそのものではなく、リフレクションなどのためのオブジェクトであり、感じとしては java.lang.reflect.Method に似ている。UnboundMethod はレシーバが切り離された Method であり、実行するためにはまずレシーバを bind して Method にしなければならない。
Ruby Extensions Project は、。
Symbol にこのような to_proc メソッドがあれば、foo メソッドを呼び出すような Proc オブジェクトが、:foo.to_proc というコードで得られる。& が付けられた実引数(前述)が、Proc オブジェクトでない場合は、to_proc メソッドが呼ばれるというコントラクトになっているので(ダックタイピング。これは前からそうなっていた)、たとえば配列の要素の合計を得る、というコードが [2, 3, 5, 7].inject :+ のように簡潔に書ける。Symbol#to_proc は、RubyKaigi 2006 の期間中、2006年6月11日に正式に Ruby に追加された。
また、Ruby においてファンクタという名前があるものとして、 プロジェクトによって導入された委譲の実装がある。委譲の最も基本的な定義は下記のようなものである:
冒頭にも述べたようSmalltalkでは関数オブジェクトに類似する機能としてブロックが存在する。他の言語同様関数的な使い方も可能であるが、複数の実行方法を備えており多機能になっている。Smalltalkでは言語構文としてif文を始め殆どの制御構文が存在せず、メッセージとこのブロックの組み合わせによってあらゆる制御を実現しておりブロックはSmalltalkの根幹をなしている。
下記に一般的なブロックの使用例を示す。[から]までがブロックである。上記における^は他の言語のreturnに当たる復帰文であるが、この復帰文はブロックの呼び出し階層を無視しブロックを含むメソッドの呼び出し元まで一気に戻るという特殊な働きをしており、更に他の言語と一線を隔てる特徴となっている。この復帰文によって、ブロックはより制御構文としての性格が強くなっている。
ここで説明するのは C++ 用語のファンクタではない。
より形式化したものとして、数学(圏論)における関手(ファンクタ)と同様のものを持つ言語もある。たとえば、Standard MLのfunctorは、モジュールからモジュールへのマッピング である。
HaskellのFunctorは以下のような型クラスである。
Prologでは、関数のシンボルをファンクタと呼んでいる。