関数オブジェクトとは
関数オブジェクト(function object)は、
プログラミング言語において、関数(
サブルーチンまたは
プロシージャ)をオブジェクトとして扱う概念です。手続きオブジェクトとも呼ばれます。ここで言うオブジェクトとは、
オブジェクト指向におけるオブジェクトに限らず、メモリ上に領域が確保されたものを指します。関数が
第一級オブジェクトである場合は、特に第一級関数と呼ばれます。
関数オブジェクトの呼び出し
関数と変数の名前空間が共通である言語では、通常の
サブルーチン呼び出しと同じ構文で関数オブジェクトを呼び出せる場合があります。一方、`apply`や`accept`といった特別なメソッドを通じて呼び出す必要がある言語もあります。また、変数束縛が閉じられた関数オブジェクトは
クロージャと呼ばれます。
関数オブジェクトの用途
関数オブジェクトは、コールバックを記述する際に特に役立ちます。
C言語では、コールバックには関数ポインタを使うしかありませんでしたが、状態変数を共有できないという制限がありました。関数オブジェクトは、このような制約を解消し、関数をより柔軟に扱えるようにします。
例えば、コールバック関数内で外部の状態変数を参照するには、
グローバル変数を使用したり、
構造体メンバーに状態変数へのポインタを持たせたりする必要がありました。関数オブジェクトは、これらの煩雑な設計パターンを言語機能として組み込み、関数を可搬性の高いオブジェクトとして利用できるようにします。
関数オブジェクトをサポートする言語
多くの現代的な
オブジェクト指向言語、例えばLISP、Smalltalk、
C++、
Java、C#、
Perl、
Python、
Rubyなどが関数オブジェクトまたは同等の機能をサポートしています。
関数オブジェクトの起源
関数オブジェクトの概念は、LISPで初期から研究されていました。Smalltalkでは、ブロックが関数オブジェクトの役割を果たします。例えば、
配列をソートする際に、比較のための関数オブジェクトを
引数に取ることで、柔軟なソート順序を実現できます。これは、Strategyデザインパターンの具体例であり、着脱可能な振る舞いを促進します。
C++ における関数オブジェクト
C++では、クラスや
構造体で関数呼び出し演算子`operator()`を
多重定義することで、オブジェクトを関数のように呼び出すことができます。このような
C++のオブジェクトを、ファンクタ(functor)と呼びます。
ファンクタとファンクショノイド
C++では、主要なメソッドを一つ持つオブジェクトを「ファンクショノイド」と呼び、その主要なメソッドが`operator()`であるオブジェクトを「ファンクタ」と呼びます。これは
C++独自の用語であり、他の文脈では通用しないことに注意が必要です。
C++での関数オブジェクトの利点
関数オブジェクトを利用すると、関数ポインタと比較して、コンパイラの最適化(インライン化)が容易になるため、パフォーマンスが向上する可能性があります。また、関数オブジェクトは状態を保持できるため、より複雑な処理を記述するのに適しています。
例: 配列要素のカウント
C++で関数オブジェクトを利用して
配列要素をカウントする例を示します。
cpp
// Cでの関数ポインタを使った実装
int countIf(int
array, int size, int (predicate)(int)) {
int count = 0;
for (int i = 0; i < size; i++) {
if (predicate(array[i])) {
count++;
}
}
return count;
}
int isPositive(int value) {
return value > 0;
}
//
C++での関数オブジェクトを使った実装
template
int countIf(int array, int size, Predicate predicate) {
int count = 0;
for (int i = 0; i < size; i++) {
if (predicate(array[i])) {
count++;
}
}
return count;
}
struct IsPositive {
bool operator()(int value) {
return value > 0;
}
};
int main() {
int array[] = {1, -2, 3, -4, 5};
int size = sizeof(array) / sizeof(array[0]);
// Cでの利用例
int countC = countIf(array, size, isPositive);
// C++での利用例
IsPositive isPositiveObj;
int countCpp = countIf(array, size, isPositiveObj);
return 0;
}
この例では、C++の関数オブジェクトを使用することで、条件の判定処理を自由にカスタマイズできます。テンプレートと関数呼び出し演算子のオーバーロードにより、静的ダックタイピングが実現されているため、条件判定にはどんなオブジェクトでも渡すことができます。
インライン化の例
C++では、関数オブジェクトのインライン化により、パフォーマンスを向上させることができます。例えば、引数をインクリメントさせるシンプルな関数は関数オブジェクトとして実装できます。
cpp
// 関数オブジェクト
struct IncrementFunctor {
void operator()(int &x) { x++; }
};
// 通常の関数
void increment(int &x) { x++; }
// STLのfor_each
template
void for_each(Iterator first, Iterator last, Function f) {
for (; first != last; ++first) {
f(first);
}
}
// 適用例
int main() {
int numbers[] = {1, 2, 3};
int size = sizeof(numbers) / sizeof(numbers[0]);
// 関数オブジェクト版
IncrementFunctor incrementFunctor;
for_each(numbers, numbers + size, incrementFunctor);
// 関数ポインタ版
for_each(numbers, numbers + size, increment);
return 0;
}
関数オブジェクト版の`for_each`では、コンパイラが関数をインライン化できますが、関数ポインタ版では、関数が不定であるためインライン化できません。
その他の言語における関数オブジェクト
C#
C#ではデリゲートが関数オブジェクトに似た機能を提供します。デリゲートは、オブジェクトのインスタンスとその振る舞いであるメソッドを結び付けて管理できます。
D言語ではデリゲートとクロージャが関数オブジェクトとして利用できます。デリゲートは変数のスコープを抜けても生存し、クロージャは変数をキャプチャして利用できます。D言語では、関数リテラルやラムダ式もサポートされています。
Javaでは関数が第一級オブジェクトではないため、関数オブジェクトの代わりに、単一のメソッドを持つインタフェースを使用します。`java.lang.Runnable`や`java.util.Comparator`などがその例です。Java 8以降はラムダ式もサポートされ、関数オブジェクトをより簡潔に記述できるようになりました。
Pythonでは、関数は第一級オブジェクトであり、任意のデータと同様に扱えます。`__call__()`メソッドを持つオブジェクトは関数のように呼び出すことができます。また、ネストした関数定義も可能です。Pythonにはラムダ式もありますが、式しか記述できないという制限があります。
Lisp
Lispでは、関数は第一級オブジェクトであり、変数に代入したり、関数から返したりできます。Lispは、最初から関数リテラル(`lambda`)をサポートしており、クロージャも利用できます。Schemeでは、変数と関数で名前空間が分かれていませんが、Common Lispなどでは名前空間が分かれています。
Rubyでは、関数は存在せず、全てがメソッドです。メソッドはオブジェクトではありません。Rubyには、ブロックという手続きの表現があり、ブロックからクロージャを作成できます。また、ProcオブジェクトやMethodオブジェクトを利用して、関数オブジェクトのような振る舞いを実現できます。
Smalltalk
Smalltalkではブロックが関数オブジェクトに類似した役割を果たします。ブロックは、他の言語と同様に関数的な使い方だけでなく、制御構造としても使用されます。`^`による復帰文はブロックの呼び出し階層を無視して、メソッドの呼び出し元まで一気に戻る特殊な機能を持っています。
ファンクタ (数学的な意味)
数学(圏論)における関手(ファンクタ)も、モジュールからモジュールへのマッピングといった形で関数オブジェクトと同様の概念を持つ場合があります。例えば、Standard MLのfunctorやHaskellのFunctorなどが該当します。
まとめ
関数オブジェクトは、プログラミングにおいて関数を柔軟に扱うための強力な概念です。各言語での実装方法は異なりますが、コールバックの改善、アルゴリズムのカスタマイズ、状態の保持など、様々な場面で役立ちます。関数オブジェクトの理解は、より高度なプログラミングを行う上で不可欠です。