関数オブジェクト
出典: フリー百科事典『ウィキペディア(Wikipedia)』
関数オブジェクト(英: function object)は、ファンクタ(functor) あるいはファンクショノイド(functionoid)とも呼ばれ、コンピュータプログラミングの構文で、オブジェクトを通常の関数のように(たいていは同じ文法で)呼び出し可能にするものである。数学の圏論における関手(functor) の概念とは関連がない。
目次 |
[編集] 詳細
関数オブジェクトの典型的な用途は、より優れたコールバックを記述することである。C などの手続き型プログラミング言語にコールバックは関数へのポインタによって実現することができるが、コールバックの内外に状態変数を渡すことが難しい。この制限のために、関数の動的な振る舞いが制約されてしまう。実際には、関数オブジェクトの持つ関数は、完全なオブジェクトに対するfacadeであり、自身で状態を持つことができるため、こうした問題を解決することができる。
C++、Java、Python、Ruby、LISP などの現代的なオブジェクト指向言語は、ほとんどが関数オブジェクトの定義をサポートしており、さらに有意義な使い方をしているものもある。
[編集] 起源
関数オブジェクトをサポートした最初期の言語の一つは Smalltalk で、Smalltalk 言語に不可欠な文法であるブロック構文を用いて関数オブジェクトを実現している。たとえば、関数オブジェクトを複数のデータを格納するオブジェクトに引数として渡し、フィルタリングや並べ替えを行わせることができる。 これは、strategy デザインパターン の完全な具現化であり、抜き差し可能な振る舞いを促進するものである。
[編集] C/C++ での関数オブジェクト
二つの要素の順序関係を定義するコールバック関数を用いて並べ替えを行うルーチンの例を考えてみよう。 関数へのポインタを使用する C のプログラムは、たとえば下記のようになる:
/* Callback function */ int compare_function(int A, int B) { return (A < B); } ... /* C の並べ替え関数の定義 */ void sort_ints(int* begin_items, int num_items, int (*cmpfunc)(int, int) ); ... int main() { int items[] = {4, 3, 1, 2}; sort_ints(items, sizeof(items)/sizeof(int), compare_function); }
C++ では通常の関数の代わりに、operator() メンバー関数を定義して 関数呼び出し演算子をオーバーロードすることで、関数オブジェクトを利用することができる。C++ は クラス型関数オブジェクト と呼ばれ、以下のような形態をしている:
class compare_class { public: bool operator()(int A, int B) { return (A < B); } }; ... // C++ の並べ替え関数の定義 template <class ComparisonFunctor> void sort_ints(int* begin_items, int num_items, ComparisonFunctor c); ... int main() { int items[] = {4, 3, 1, 2}; compare_class functor; sort_ints(items, sizeof(items)/sizeof(int), functor); }
コールバックを sort_ints()
関数に渡す文法は同じだが、関数へのポインタではなくオブジェクトが渡されていることに注意しよう。
コールバック関数が実行されると、他のメンバー関数と同様に働き、すなわちオブジェクトの他のメンバー(データや関数)に対して完全にアクセスすることができる。
関数オブジェクトはコールバック関数以外の状況でも使用することができる(ファンクタとはもはや呼ばれないが)。下記のような例である:
functor_class Y; int result = Y( a, b );
クラス型の関数オブジェクトに加えて、C++ では別の種類の関数オブジェクトが可能である。
C++ のメンバーポインタや、テンプレート機能を利用することができ、テンプレートの記述力により、(関数の合成などの)別種の関数オブジェクトを定義するといったいくつかの関数型言語の技法を用いることができる。C++ の Standard Template Library (STL) では、テンプレートによる関数オブジェクトを多用している。
[編集] 性能
C++ における関数オブジェクトの利点は関数のポインタと異なり、インライン化できるためパフォーマンスが良い点である。たとえば、引数をインクリメントさせるシンプルな関数は関数オブジェクトとして実装できる:
struct IncrementFunctor { void operator()(int&i) { ++i; } };
通常の関数:
void increment_function(int&i) { ++i; }
STL 関数 std::for_each()
を用いると下記のようになる:
template<typename InputIterator, typename Function> Function for_each(InputIterator first, InputIterator last, Function f) { for ( ; first != last; ++first) f(*first); return f; }
ここに std::for_each()
を適用すると下記のようになる:
int A[] = {1, 4, 2, 8, 5, 7}; const int N = sizeof(A) / sizeof(a[0]); for_each(A, A + N, IncrementFunctor()); for_each(A, A + N, increment_function);
いずれの for_each()
も期待通りの動作をするが、最初の方法では以下のように展開される。
IncrementFunctor for_each<int*,IncrementFunctor>(int*, int*, IncrementFunctor)
二番目の方法では以下のように展開される。
void(*)(int&) for_each<int*,void(*)(int&)>(int*, int*, void(*)(int&))
for_each<int*,IncrementFunctor>()
の場合には関数が既知であるためコンパイラがインライン化できるが、for_each<int*,void(*)(int&)>()
の場合にはコンパイル時に関数が不定でありインライン化できない。
現実には、コンパイラに指示されれば簡単に関数を既知にすることができる。コンパイラが関数の定義を認識しており、それがクラスの内外いずれでも同じように行われていさえすればよい。インライン化しない場合、リンカは関数がクラスの関数だとの指示さえあれば同じ関数の別のコンパイル単位の複数回の定義を エラーを生成せずに黙って見過ごす。リンカは同じ関数の定義がクラスの関数でない場合には複数回の定義を許容しないためである。
[編集] 状態の保持
関数オブジェクトの利点の一つは、関数の呼び出しをまたいで状態を(オブジェクトのフィールドとして)保持できる点である。たとえば、下記のコードは10以上の数を数えるジェネレータ(引数をとらない関数)を定義し、11 回呼び出し結果を出力している。
#include <iostream> #include <iterator> #include <algorithm> class countfrom { private: int count; public: countfrom(int n) : count(n) {} int operator()() { return count++; } }; int main() { std::generate_n(std::ostream_iterator<int>(std::cout, "\n"), 11, countfrom(10)); return 0; }
[編集] D言語の関数オブジェクト
D言語 は関数オブジェクトを宣言する複数の方法を提供している。それぞれ Lisp/Python スタイルのクロージャを用いた方法と、C# スタイルのデリゲートを用いた方法である。
bool find(T)(T[] haystack, bool delegate(T) needle_test) { foreach ( straw; haystack ) { if ( needle_test(straw) ) return true; } return false; } void main() { int[] haystack = [345,15,457,9,56,123,456]; int needle = 123; bool needleTest(int n) { return n == needle; } assert( needle == find(haystack, &needleTest) ); }
D 言語におけるデリゲートとクロージャの違いは、コンパイラにより保守的な方法で自動的に決定される。
D 言語は、関数リテラルもサポートしており、ラムダ形式の定義が可能である。
void main() { int[] haystack = [345,15,457,9,56,123,456]; int needle = 123; assert( needle == find(haystack, (int n) { return n == needle; }) ); }
コンパイラがインライン化できるようにするため(上記参照)、関数オブジェクトを C++ 形式の演算子のオーバーロードを用いて宣言することもできる。
bool find(T,F)(T[] haystack, F needle_test) { foreach ( straw; haystack ) { if ( needle_test(straw) ) return true; } return false; } void main() { int[] haystack = [345,15,457,9,56,123,456]; int needle = 123; class NeedleTest { int needle; this(int n) { needle = n; } bool opCall(int n) { return n == needle; } } assert( needle == find(haystack, new NeedleTest(needle)) ); }
[編集] Java における関数オブジェクト
Javaでは関数が第一級オブジェクトでないため、関数オブジェクトは通常一つのメソッドを持つインタフェースとして表現され、通常は無名のインナークラスとして実装される。
Java の標準ライブラリの例では、java.util.Collections.sort() はリストと関数オブジェクトを引数にとり、関数オブジェクトはリスト内のオブジェクトを比較する役割を持つ。しかし、Java は関数が第一級オブジェクトでないため、関数は Comparator インタフェースを実現したものになる。下記の例のように使用する:
List<String> list = Arrays.asList(new String[] { "10", "1", "20", "11", "21", "12" }); Collections.sort(list, new Comparator<String>() { public int compare(String o1, String o2) { return Integer.valueOf(o1).compareTo(Integer.valueOf(o2)); } });
[編集] Python における関数オブジェクト
Python では、文字列や数値、リストなどと同様に関数がオブジェクトである。この機能により多くの場面で関数オブジェクトを作成する必要がない。しかし、__call__()
メソッドを持つ任意のオブジェクトを関数呼び出しの文法で呼び出すことができる。
例として、Accumulator クラス(ポール・グレアムのプログラミング言語の文法と明快さの研究[1]に登場する) を挙げる。
class Accumulator(object): def __init__(self, n): self.n = n def __call__(self, x): self.n += x return self.n
下記のように使用する(対話的インタプリタを用いている):
>>> a = Accumulator(4)
>>> a(5)
9
>>> a(2)
11
>>> b = Accumulator(42)
>>> b(7)
49
Python で関数オブジェクトを定義するもう一つの方法としてクロージャを用いる方法がある:
def Accumulator(n): def inc(x): inc.n += x return inc.n inc.n = n return inc
[編集] Lisp における関数オブジェクト
Common Lisp、Scheme などの Lisp 系の言語では、文字列やベクトル、リスト、数値などと同様に関数がオブジェクトである。クロージャを作成する演算子が、プログラム自体から関数オブジェクトを生成する。演算子の引数として与えられたコードブロックが関数の一部であり、構文上明らかに見える変数は 関数オブジェクト内に捕捉され格納され、これはより一般的に クロージャ と呼ばれる。
捕捉された振る舞いは「メンバー変数」の役割を果たし、クロージャのコード部分は C++ の演算子() のように「無名のメンバー関数」の役割を果たす。
クロージャの構文には、(lambda (パラメータ ...) コード ...)
の文法を用いる。 (パラメータ ...)
部分で、関数が宣言されたパラメータを受け取れるよう、インターフェイスの宣言を行うことができる。コード ...
部分は関数オブジェクトが呼び出されたときに評価される式からなる。
C++ のような言語での関数オブジェクトの使用方法の多くは、存在しないクロージャ構文をまねたものである。これらの言語では、プログラマーはクロージャを直接作成することができないため、必要な変数やメンバ関数を全て備えたクラスを定義しなければらない。そして、クロージャの代わりにクラスのインスタンスを生成し、全てのメンバー変数がコンストラクタで初期化されることを保証しなければならない。クロージャであれば直接捕捉できるが、変数の値は、ローカル変数から受け継がれる。
クラスシステムを用いた関数オブジェクトで、クロージャを使用しない場合:
(defclass counter () ((value :initarg :value :accessor value-of))) (defmethod functor-call ((c counter)) (incf (value-of c))) (defun make-counter (initial-value) (make-instance 'counter :value initial-value)) ;;; カウンタの使用 (defvar *c* (make-counter 10)) (functor-call *c*) --> 11 (functor-call *c*) --> 12
Lisp では関数オブジェクトを作成する標準的な方法はないため、FUNCTOR-CALL と呼ばれる汎用関数を定義して模倣している。これはいかなるクラスにも適用できる。標準の FUNCALL 関数は汎用ではなく、関数オブジェクトのみ扱うことができる。
関数オブジェクトの機能を与えるのはこの FUNCTOR-CALL 汎用関数であり、関数オブジェクトは 「コンピュータプログラミングの構文で、オブジェクトを通常の関数のように(たいていは同じ文法で)呼び出し可能なもの」である。ここで、FUNCALL でなく FUNCTOR-CALL という点で、ほとんど 同じ文法である。Lisp のうち、シンプルな拡張で "関数呼び出し可能な" オブジェクトを提供しているものもある。 オブジェクトを関数と同じ文法で呼び出し可能にすることはたいした問題ではない。関数の呼び出し演算子が複数の"関数オブジェクト的要素" を扱い、それがクラスのオブジェクトであるか、クロージャであるかは、"+" 演算子が整数や実数、複素数などの複数の種類の数値を扱うことができるのと同程度の問題である。
ここで、カウンターがクロージャを使って実装されている。簡単な例として、MAKE-COUNTER ファクトリー関数のINITIAL-VALUE 引数は補足され直接使用される。補助的なクラスオブジェクトから、コンストラクタを介してコピーする必要はない。これはカウンターである。 補助的なクラスオブジェクトは生成されるが、舞台裏でこっそりと行われる。
(defun make-counter (initial-value) (lambda () (incf initial-value))) ;;; use the counter (defvar *c* (make-counter 10)) (funcall *c*) --> 11 (funcall *c*) --> 12
二つ以上のクロージャを同じ構文で作成することができる。クロージャの配列で、それぞれが独自の操作を実現したものは、仮想関数を持つオブジェクトを忠実に再現することができる。シングルディスパッチのオブジェクト指向プログラミングは、クロージャで完全に実現することができる。
このため、伝説の山の両方から掘られたトンネルが存在する。OOP 言語のプログラマは、関数オブジェクトをオブジェクトが唯一の"main"関数を持ち、直接呼び出されるように見せるためオブジェクトが名前すら持たないようにしてしまうような、制約を行うためのオブジェクトとして価値を見出す。 クロージャを用いるプログラマー達は、オブジェクトが関数的に呼び出されることは自然なことで、同じ環境を共有する複数のクロージャがシングルディスパッチの OOP における仮想関数テーブルのような抽象的操作のセットを提供できることを見出した。
[編集] Ruby における関数オブジェクト
Ruby には Method や Proc といった関数オブジェクトとみなせる多数のオブジェクトが存在する。Ruby はまた、関数オブジェクトに近い二種類のオブジェクト、UnboundMethod とブロックがある。UnboundMethod は関数オブジェクトとして用いる前にオブジェクトに関連付けられていなければならない(したがって Methodとなる)。ブロックは関数オブジェクトのように呼び出すことが可能だが、オブジェクトとして他の箇所でも使用できる(関数の引数として渡すなど)ようにするためには、まず Proc に変換しなければならない。つい最近には、シンボル(リテラルの単項演算子 :
を介してアクセスされる)も Proc
に変換することができるようになった。Ruby の単項 &
演算子(to_proc
を呼び出すのと等価であり、メソッドの存在を仮定している)を用いて、Ruby Extensions Project は、シンプルなハックを開発した。
class Symbol def to_proc proc { |obj, *args| obj.send(self, *args) } end end
ここで、foo
メソッドは&:foo
を用いて関数オブジェクト、すなわちProc
になることができ、takes_a_functor(&:foo)
という形式で使用することができる。Symbol.to_proc
は、RubyKaiga2006 の期間中、2006年6月11日に正式に Ruby に追加された[2]。
形態が様々であるため、Ruby では Functor という名称は関数オブジェクトを示すために用いられない。むしろ、Ruby Facets プロジェクトによって導入された委譲を示すようになってきている。委譲の最も基本的な定義は下記のようなものである:
class Functor def initialize(&func) @func = func end def method_missing(op, *args, &blk) @func.call(op, *args, &blk) end end
この使用方法は、ML のような関数型プログラミング言語やもともとの数学における意味合いに近い。
[編集] 関数オブジェクト(functor)のその他の意味
ML のような関数型言語では、関数オブジェクト(functor)は、モジュールからモジュールへのマッピング であり、コードを再利用するための技法である。この意味での関数オブジェクト(functor)はもともとの数学における圏論における関手(functor)の意味や、C++ のテンプレートの用いることに近い。
より理論的な意味では、「関数オブジェクト」は、とくに、Common Lisp のような関数が第一級オブジェクトであるような言語では、"関数のクラス"のインスタンスと考えることができる。このような場合に"functor"という用語が用いられることはめったにない。
Prologや関連する言語では、"functor"は関数のシンボルと同義である。
[編集] 関連項目
- クロージャ
- Commandパターン
[編集] 参考文献
- Vandevoorde, David, & Josuttis, Nicolai M. C++ Templates: The Complete Guide, ISBN 0-201-73484-2
(Specifically, chapter 22 is entirely devoted to function objects.)
[編集] 外部リンク
- Description from the Portland Pattern Repository
- "C++ Advanced Design Issues - Asynchronous C++" by Kevlin Henney
- The Function Pointer Tutorials by Lars Haendel (2000/2001)
- Article "Generalized Function Pointers" by Herb Sutter
- Generic Algorithms for Java