関数オブジェクト(かんすうオブジェクト、)は、プログラミング言語において、関数(サブルーチンまたはプロシージャ)を、オブジェクトとしたものである。。なお、ここでのオブジェクトの語は、いわゆるオブジェクト指向のそれに限らず、「第一級オブジェクト」という語におけるのと同じ、メモリ上に領域を確保されたもの、といった意味である。関数が第一級オブジェクトである場合は特に第一級関数と言う。
、構文の設計によっては、y = f(x) といったような、通常のサブルーチン呼び出しと全く同じ構文で、関数オブジェクトが保持しているサブルーチンを呼び出せる言語もある。一方、通常のサブルーチンのように呼び出すことはできず、applyやacceptやgetといった特別な名前のメソッドを経由して呼び出す必要のある言語もある。
また、変数束縛が閉じられた関数オブジェクトはクロージャである。C#などの.NET言語には関数オブジェクトのようなものとして、オブジェクトのインスタンスとその振る舞いであるメソッドとを結びつけて管理することのできる、デリゲートがある。無名関数も参照。
用途 関数オブジェクトの典型的な用途は、より優れたコールバックを記述することである。C言語では、コールバックには関数へのポインタを使う他ないが、コールバックの内外で状態変数を共有できない。この制限のために、関数の動的な振る舞いやインターフェイス設計が制約されてしまう。以下に例を示す。 /* ライブラリ側のコード */ typedef void my_callback_func_t(void); typedef struct my_handler { my_callback_func_t* callback; } my_handler_t;
void register_callback(my_handler_t* handler, my_callback_func_t* func) { handler->callback = func; }
void invoke_callback(my_handler_t* handler) { if (handler->callback) { handler->callback();
void perform_process(my_handler_t* handler) { /* 前処理 */ /* ... */ /* コールバック関数の呼び出し */ invoke_callback(handler); /* 後処理 */ /* ... */ }
/* アプリケーション側のコード */ void callback_func(void) { /* ... */ /* ここで後述の状態変数 state にアクセスしたくても、不可能 */ }
int main(void) { /* 状態変数 */ static int state = 0;
my_handler_t handler = { NULL }; /* コールバックとして callback_func という関数へのポインタを登録 */ register_callback(&handler, &callback_func);
perform_process(&handler); } コールバック関数内で外部の状態変数を参照するためには、グローバル変数を利用したり、構造体メンバーに状態変数へのポインタを持たせたうえでコールバック関数の引数を経由して状態変数のアドレスを渡したり、といった工夫が必要になる。
関数オブジェクトは、上記のように煩雑になりがちな設計パターンを言語機能として組み込み、関数を可搬性の高いオブジェクトとして容易に利用できるようにするものである。
LISPや、Smalltalk、C++、Java、C#、Perl、Python、Rubyなどの現代的なオブジェクト指向言語は、ほとんどが関数オブジェクト、あるいは同等の機能をサポートしており、。
起源 関数オブジェクトは、LISPにおいてその初期から研究された。計算機プログラムの構造と解釈の第3章でも解説されている。
オブジェクト指向言語では、Smalltalk において、ブロックが関数オブジェクトの記法となるよう設計された。たとえば配列の各要素を通常の大小関係とは違う順序で並べ替えたい場合、比較のための関数オブジェクトを引数に取るソートメソッドを、引数としてブロックを付けて、呼び出す。ブロック内には、カスタマイズ版の比較手続きを記述する。ソートメソッド内での比較は、渡された関数オブジェクトの手続きを呼び出すことで行なわれ、期待する大小関係でのソートが行なわれる。これは、Strategyデザインパターンの完全な具現化であり、着脱可能 (pluggable) な振る舞いを促進するものである。
C++ での関数オブジェクト C++では、クラスあるいは構造体において関数呼び出し演算子の多重定義が可能となっている。具体的には、任意の引数を持つoperator()メンバー関数を定義することで、そのオブジェクトのインスタンスを指す変数の名前が、あたかも関数名(関数指示子)であるかのような構文で、定義した関数を呼ぶことができる。このような C++ のオブジェクトを、C++ の用語ではファンクタ (functor) と呼ぶ(#C++のファンクタとファンクショノイド を参照)。 struct my_adder { int operator()(int a, int b) const { return a + b;
my_adder f; int result = f(2, 3); // 以下のように呼び出すことも可能だが、通例使われない。 f.operator()(2, 3); C++のclass/structは、デフォルトのアクセスレベル()がprivate/publicであるという違いしかないため、関数オブジェクトの記述ではclassの代わりにstructが使われることも多い。
配列の中から特定の条件に該当する要素の個数を数えるルーチンの例を考えてみよう。ただし条件の判定処理は自由にカスタマイズできるものとする。
関数へのポインタを使用する C のプログラムは、たとえば下記のようになる。 #include <stdio.h>
/* 引数が条件に該当する場合は 1 を、該当しない場合は 0 を返す */ typedef int PredicateFunc(int x);
size_t countIf(const int data[], size_t num, PredicateFunc* predicate) { size_t count = 0; size_t i; for (i = 0; i < num; ++i) { if (predicate(data[i])) { count++; }
return count; }
int isPositive(int x) { return x > 0; }
int isEven(int x) { return x % 2 == 0; }
int main(void) { const int data[] = { 0, 1, 2, 3, 4, 5, 6, 0, -1, -2, -3, -4 }; printf("Count of positive numbers = %ld\n", (long)countIf(data, sizeof(data) / sizeof(*data), &isPositive)); printf("Count of even numbers = %ld\n", (long)countIf(data, sizeof(data) / sizeof(*data), &isEven)); }
一方、C++の関数オブジェクトを利用すると下記のようになる。 #include <cstdio>
template<typename TPredicate> size_t countIf(const int data[], size_t num, TPredicate predicate) { size_t count = 0; for (size_t i = 0; i < num; ++i) { if (predicate(data[i])) { count++; }
return count; }
struct IsPositiveFunctor { bool operator()(int x) const { return x > 0; }
struct IsEvenFunctor { bool operator()(int x) const { return x % 2 == 0; }
int main() { const int data[] = { 0, 1, 2, 3, 4, 5, 6, 0, -1, -2, -3, -4 }; std::printf("Count of positive numbers = %ld\n", (long)countIf(data, sizeof(data) / sizeof(*data), IsPositiveFunctor())); std::printf("Count of even numbers = %ld\n", (long)countIf(data, sizeof(data) / sizeof(*data), IsEvenFunctor())); } コールバックを述語 (predicate) として countIf() 関数テンプレートに渡す際、関数へのポインタではなく構造体のインスタンス(関数オブジェクト)を渡していることに注意されたい。
上記の例において関数オブジェクトによるカスタマイズを可能にしているのは、テンプレートと関数呼び出し演算子のオーバーロードによる静的ダック・タイピングである。関数テンプレートを実体化する際の字句解析に適合しさえすれば、述語にはどんなオブジェクトでも渡せる。
コールバック関数が実行されると、他のメンバー関数と同様に働き、すなわちオブジェクトの他のメンバー(データや関数)に対して完全にアクセスすることができる。
C++ の Standard Template Library (STL) では、テンプレートの述語 (predicate) として関数オブジェクトを多用している。
C++のファンクタとファンクショノイド 以上のように C++ では、関数オブジェクトは、「関数呼び出しと同じ構文で、メンバ関数を呼ぶことができるオブジェクト」として実装されている。これを C++ の用語ではファンクタと呼んでいるが、これはStandard MLのfunctorや、数学における関手とは関係ない(と考えたほうが良い)。
C++ では、主要なメソッド一つを持つオブジェクトを「ファンクショノイド」と言い、その一種で、その主要なメソッドが operator() であるオブジェクトが「ファンクタ」である、と説明されるC++ FAQ [33.15] What's the difference between a functionoid and a functor?。これは C++ の用語であり、C++ を離れた文脈では、関数オブジェクトすなわちファンクタ、ではないので注意。
性能 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 size_t N = sizeof(A) / sizeof(A); 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&)>() の場合にはコンパイル時に関数が不定でありインライン化できない。
現実には、コンパイラに指示すれば簡単に関数を既知にすることができる。コンパイラが関数の定義を認識しており、それがクラスの内外いずれでも同じように行われていさえすればよい。インライン化しない場合、リンカは関数がクラスの関数だとの指示さえあれば同じ関数の別のコンパイル単位の複数回の定義をエラーを生成せずに黙って見過ごす。リンカは同じ関数の定義がクラスの関数でない場合には複数回の定義を許容しないためである。
もうひとつの例として、std::sort()関数テンプレートを挙げる。 #include <iostream> #include <algorithm> #include <cstdlib> #include <vector> #include <chrono>
void createRandomNumbers(std::vector<int>& array, int seed) { std::srand(seed); for (auto&& x : array) { x = std::rand(); } }
inline bool compareFunc(const int& a, const int& b) { return a < b; }
struct CompareFunctor { bool operator()(const int& a, const int& b) const { return a < b; }
template<typename TFunc> void performFunc(TFunc func) { const auto startTime = std::chrono::system_clock::now(); func(); const auto endTime = std::chrono::system_clock::now(); std::cout << "Elapsed time [ms] = " << std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count() << std::endl; }
int main() { std::vector<int> array(1000 * 1000); std::cout << "* std::qsort:" << std::endl; createRandomNumbers(array, 0); performFunc([&array]() { std::qsort(array.data(), array.size(), sizeof(int), [](const void* a, const void* b) { return *static_cast<const int*>(a) - *static_cast<const int*>(b); }); }); std::cout << "* std::sort and function pointer:" << std::endl; createRandomNumbers(array, 0); performFunc([&array]() { std::sort(array.begin(), array.end(), compareFunc); }); std::cout << "* std::sort and functor:" << std::endl; createRandomNumbers(array, 0); performFunc([&array]() { std::sort(array.begin(), array.end(), CompareFunctor()); }); std::cout << "* std::sort and lambda:" << std::endl; createRandomNumbers(array, 0); performFunc([&array]() { std::sort(array.begin(), array.end(), [](const int& a, const int& b) { return a < b; }); }); } std::qsort()およびstd::sort()の実装はいずれもクイックソートとは限らず、処理系依存のため、直接性能を比較することに意味はないが、通例インライン展開が利用できるstd::sort()のほうが性能が良くなる。また、述語に関数ポインタを利用するよりも、関数オブジェクトを利用したほうが性能が良くなる。C++11で追加されたラムダ式は、コンパイラによって内部的には関数オブジェクトの定義に展開されるため、通例関数オブジェクトを利用した場合と同等の性能になる。
状態の保持
関数オブジェクトの利点の一つは、関数の呼び出しをまたいで状態を(オブジェクトのフィールドとして)保持できる点である。たとえば、下記のコードは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; } 関数オブジェクトで状態を保持する場合、明示的にメンバー変数を定義したうえで、関数オブジェクトのメンバーとして操作を記述しなければならなかった。C++11で導入されたラムダ式では、外部の変数のキャプチャをサポートし、クロージャを実現することができる。ラムダ式によってキャプチャされた変数は、コンパイラによって暗黙的に生成される関数オブジェクトのメンバー変数となる。
C#の関数オブジェクト
D言語の関数オブジェクト D言語には関数オブジェクトとして、デリゲートとクロージャの両方がある。 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(find(haystack, &needleTest)); } D言語におけるデリゲートとクロージャの違いは、コントロールが変数のスコープから一旦抜けても、変数の寿命が続いているか、そうでないかである。コンパイラにより保守的に、自動的に決定される(後から変数を参照する可能性があればクロージャとする)。
D言語は、関数リテラルやラムダ式もサポートしている。 void main() { int[] haystack = [345, 15, 457, 9, 56, 123, 456]; int needle = 123; assert(find(haystack, (int n) { return n == needle; })); assert(find(haystack, (int n) => n == needle)); } コンパイラがインライン化できるようにするため(上記参照)、関数オブジェクトをC++形式の演算子のオーバーロードを用いて宣言することもできる。しかし、D言語ではテンプレート引数として関数等を渡す手法が一般的である。 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;
struct NeedleTest { int needle; this(int n) { needle = n; } bool opCall(int n) { return n == needle;
assert(find(haystack, NeedleTest(needle))); }
Java における関数オブジェクト Javaでは関数が第一級オブジェクトでないため、関数オブジェクトの代わりとして、一つのメソッドを持つインタフェースが使われる。代表的なインタフェースとしては、run()メソッドを持つが挙げられる。実際のコーディングにおいては、そのようなインタフェースを実装するクラスの定義には、内部クラス (inner class) や、メソッド内の変数をキャプチャしてクロージャのように振る舞うことのできるローカルクラス (local class) あるいは匿名クラス (anonymous class) がしばしば使われる。
Javaの標準ライブラリの例では、 はリストとコンパレータ(意味的には関数)を引数にとる。第2引数のコンパレータはリスト内のオブジェクトを比較する役割を持つ。しかし、Javaは関数が第一級オブジェクトでないため、インタフェースを実装したオブジェクトを渡す。下記の例のように使用する:
List<String> list = Arrays.asList(new String[] { "10", "1", "20", "11", "21", "12" });
// Comparator を実装する匿名クラスを定義して利用。 Collections.sort(list, new Comparator<String>() { public int compare(String o1, String o2) { return Integer.valueOf(o1).compareTo(Integer.valueOf(o2));
); Java 8からは、単一の抽象メソッドを持つインタフェース(関数型インタフェース)を実装する匿名クラスの糖衣構文としてラムダ式をサポートする。ラムダ式を利用すると、以下のように簡潔に記述することができる。 Collections.sort(list, (String o1, String o2) -> Integer.valueOf(o1).compareTo(Integer.valueOf(o2)) ); ラムダ式の引数の型指定は省略することも可能である。Java 11以降は予約型名varを使用することもできるようになったイマドキのJava徹底入門(13) ラムダ式の入力パラメータにvarを使用する | TECH+(テックプラス)。
また、Java 8はメソッド参照もサポートするようになった。メソッド参照は、内部的にはインタフェースを利用して実装されている。
パッケージには、ラムダ式やメソッド参照のターゲットとして利用可能な汎用の関数型インタフェースがいくつか定義されており、これらを利用すればユーザーコードでインタフェースを明示的に定義する手間を省くことができる。
Python における関数オブジェクト Pythonでは、関数は、文字列や数値、リストなどをはじめとする他の任意のデータと同様のオブジェクトである。つまり、Pythonは第一級関数を扱うことができる言語である。また、__call__() メソッドを持つ任意のオブジェクトを関数呼び出しの構文で呼び出すことができる。
例として、Accumulator クラス (ポール・グレアムのプログラミング言語の文法と明快さの研究に登場する) を挙げる。 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 一方で Python には、その内部に式しか書くことができない(文が書けない)という強い制限のある lambda form しか関数リテラルに相当するものがなく、手続き的なものは名前を付けて定義しなければならない。これはそうするのが良いプラクティスとされているからである。
Lisp における関数オブジェクト Lisp においても、関数は、文字列やベクトル・リスト・数値と同様に変数に入れたり関数から返したりできる第一級オブジェクトであり、第一級関数を扱うことができる言語である。