関数オブジェクト
関数オブジェクト(かんすうオブジェクト、英: function object)は、プログラミング言語において、関数(サブルーチンまたはプロシージャ)を、オブジェクトとしたものである。手続きオブジェクトとも言う(プロシージャ=手続き)[要出典]。なお、ここでのオブジェクトの語は、いわゆるオブジェクト指向のそれに限らず、「第一級オブジェクト」という語におけるのと同じ、メモリ上に領域を確保されたもの、といった意味である。関数が第一級オブジェクトである場合は特に第一級関数と言う。 関数と変数の名前空間が共通である言語の場合[要追加記述]、構文の設計によっては、 また、変数束縛が閉じられた関数オブジェクトはクロージャである。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++では、クラスあるいは構造体において関数呼び出し演算子の多重定義が可能となっている。具体的には、任意の引数を持つ 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) として 上記の例において関数オブジェクトによるカスタマイズを可能にしているのは、テンプレートと関数呼び出し演算子のオーバーロードによる静的ダック・タイピングである。関数テンプレートを実体化する際の字句解析に適合しさえすれば、述語にはどんなオブジェクトでも渡せる。 コールバック関数が実行されると、他のメンバー関数と同様に働き、すなわちオブジェクトの他のメンバー(データや関数)に対して完全にアクセスすることができる。 クラス型の関数オブジェクトに加えて、C++ では別の種類の関数オブジェクトが可能である。[要校閲] C++ のメンバーポインタや、テンプレート機能を利用することができ、テンプレートの記述力により、(関数の合成などの)別種の関数オブジェクトを定義するといったいくつかの関数型言語の技法を用いることができる。[要校閲] C++ の Standard Template Library (STL) では、テンプレートの述語 (predicate) として関数オブジェクトを多用している。 C++のファンクタとファンクショノイド以上のように C++ では、関数オブジェクトは、「関数呼び出しと同じ構文で、メンバ関数を呼ぶことができるオブジェクト」として実装されている。これを C++ の用語ではファンクタと呼んでいるが、これはStandard MLのfunctorや、数学における関手とは関係ない(と考えたほうが良い)。 C++ では、主要なメソッド一つを持つオブジェクトを「ファンクショノイド」と言い、その一種で、その主要なメソッドが operator() であるオブジェクトが「ファンクタ」である、と説明される[1]。これは C++ の用語であり、C++ を離れた文脈では、関数オブジェクトすなわちファンクタ、ではないので注意。 性能C++における関数オブジェクトの利点のひとつは、関数ポインタと異なり、コンパイラの最適化によってインライン化されやすいため、パフォーマンスが向上する可能性が高くなるという点である。たとえば、引数をインクリメントさせるシンプルな関数は関数オブジェクトとして実装できる: struct IncrementFunctor {
void operator()(int& i) { ++i; }
};
通常の関数: void increment_function(int& i) { ++i; }
STL 関数 template<typename InputIterator, typename Function>
Function for_each(InputIterator first, InputIterator last, Function f) {
for ( ; first != last; ++first)
f(*first);
return f;
}
ここに int A[] = {1, 4, 2, 8, 5, 7};
const size_t N = sizeof(A) / sizeof(A[0]);
for_each(A, A + N, IncrementFunctor());
for_each(A, A + N, increment_function);
いずれの IncrementFunctor for_each<int*, IncrementFunctor>(int*, int*, IncrementFunctor)
二番目の方法では以下のように展開される。 void(*)(int&) for_each<int*, void(*)(int&)>(int*, int*, void(*)(int&))
現実には、コンパイラに指示すれば簡単に関数を既知にすることができる。コンパイラが関数の定義を認識しており、それがクラスの内外いずれでも同じように行われていさえすればよい。インライン化しない場合、リンカは関数がクラスの関数だとの指示さえあれば同じ関数の別のコンパイル単位の複数回の定義をエラーを生成せずに黙って見過ごす。リンカは同じ関数の定義がクラスの関数でない場合には複数回の定義を許容しないためである。 もうひとつの例として、 #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; });
});
}
状態の保持関数オブジェクトの利点の一つは、関数の呼び出しをまたいで状態を(オブジェクトのフィールドとして)保持できる点である。たとえば、下記のコードは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では関数が第一級オブジェクトでないため、関数オブジェクトの代わりとして、一つのメソッドを持つインタフェースが使われる[注釈 1]。代表的なインタフェースとしては、 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以降は予約型名 また、Java 8はメソッド参照もサポートするようになった。メソッド参照は、内部的にはインタフェースを利用して実装されている。
Python における関数オブジェクトPythonでは、関数は、文字列や数値、リストなどをはじめとする他の任意のデータと同様のオブジェクトである。つまり、Pythonは第一級関数を扱うことができる言語である。また、 例として、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
一方で Python には、その内部に式しか書くことができない(文が書けない)という強い制限のある lambda form しか関数リテラルに相当するものがなく、手続き的なものは名前を付けて定義しなければならない。これはそうするのが良いプラクティスとされているからである。 Lisp における関数オブジェクトLisp においても、関数は、文字列やベクトル・リスト・数値と同様に変数に入れたり関数から返したりできる第一級オブジェクトであり、第一級関数を扱うことができる言語である。 Lisp はその最初から Scheme では変数と関数で名前空間が分かれておらず、変数名を関数名と同様に使ってプログラムを書くことができる。 (define (hello s)
(print (format "hello, ~a" s)) )
(hello "world")
(let ((f hello))
(f "Scheme") )
これを実行すると、 hello, world hello, Scheme のように出力される。 これに対し、伝統的な Lisp の多くや Common Lisp は変数と関数で名前空間が分かれている。同様のプログラムを Common Lisp で書いた例を示す。 (DEFUN HELLO (S)
(PRINT (FORMAT NIL "hello, ~A" S)) )
(HELLO "world")
(LET ((F #'HELLO))
(FUNCALL F "Common Lisp") )
これを実行すると、 hello, world hello, Common Lisp のように出力される。 Common Lisp では、名前に対して、変数としての値と、関数とが別々に結びつけられていて、文脈により(カッコ内の並びの先頭にあるか、そうでないかにより)どちらかがアクセスされる。F という変数に関数値を束縛する所では、通常の文脈において関数にアクセスするために 変数と関数の名前空間を分けるか同じにするか、という議論は、Lisp に限らず現代的なプログラミング言語の設計において話題になる。ML などの現代的な関数型言語では当然のように同じ名前空間である。スクリプティング言語では、Python や JavaScript は同じ名前空間としたが、Ruby ではローカル変数とメソッドで別の名前空間とした。Lisp では「Lisp-1 対 Lisp-2 の議論」などと呼ばれる(en:Common Lisp#The function namespace を参照)。 Ruby における関数オブジェクトRuby では、便宜上 Object クラスのローカルインスタンスメソッドを「グローバル関数」と呼んでいるといった例外はあるが、関数は存在せず、全てメソッドである。メソッドはオブジェクトではなく、変数とメソッドで名前空間が違う。 Ruby には、メソッドの他に手続きの表現としてブロックがある。Ruby のブロックは、メソッド呼び出しにオプショナルに付加できるもので、暗黙の引数といったような感じで呼び出されるメソッドに渡される。ブロックからクロージャが作られ、呼び出された側からは yield という特殊なグローバル関数により、そのクロージャを呼ぶことができる。 ブロックは直接にはオブジェクトではない。しかし、メソッド定義の仮引数の記述の最後に、 Proc オブジェクトは文脈という環境を持つ関数オブジェクト状のものである。これに対し、レシーバ( Ruby Extensions Project は、シンプルなハックを開発した。 class Symbol
def to_proc
proc { |obj, *args| obj.send(self, *args) }
end
end
Symbol にこのような to_proc メソッドがあれば、 また、Ruby においてファンクタという名前があるものとして、Ruby Facets プロジェクトによって導入された委譲の実装がある。委譲の最も基本的な定義は下記のようなものである: class Functor
def initialize(&func)
@func = func
end
def method_missing(op, *args, &blk)
@func.call(op, *args, &blk)
end
end
Smalltalkにおける関数オブジェクト冒頭にも述べたようSmalltalkでは関数オブジェクトに類似する機能としてブロックが存在する。他の言語同様関数的な使い方も可能であるが、複数の実行方法を備えており多機能になっている。Smalltalkでは言語構文としてif文を始め殆どの制御構文が存在せず、メッセージとこのブロックの組み合わせによってあらゆる制御を実現しておりブロックはSmalltalkの根幹をなしている。
下記に一般的なブロックの使用例を示す。 Number methodsFor:'comparing'
!
max: aNumber
"レシーバーと引数を比較して大きい方を返す。"
self < aNumber
ifTrue:
[
^ aNumber. "ブロック内ではブロックの外側にある変数も参照できる"
]
ifFalse:
[
^ self.
].
!!
上記における ファンクタここで説明するのは C++ 用語のファンクタではない。 より形式化したものとして、数学(圏論)における関手(ファンクタ)と同様のものを持つ言語もある。たとえば、Standard MLのfunctorは、モジュールからモジュールへのマッピング である。 HaskellのFunctorは以下のような型クラスである。 class Functor f where
fmap :: (a -> b) -> f a -> f b
-- さらに、型 f α と f α に対して定義された fmap は、以下を満たさなければならない。
-- 恒等関数を恒等関数にうつす
-- fmap id == id
-- 関数合成との関係
-- fmap (func1 . func2) == fmap func1 . fmap func2
文献
脚注注釈
出典
外部リンク
|
Portal di Ensiklopedia Dunia