関数オブジェクト

出典: フリー百科事典『ウィキペディア(Wikipedia)』
移動: 案内検索

関数オブジェクト(かんすうオブジェクト、: function object)は、プログラミング言語において、関数(サブルーチンないしプロシージャ)を、オブジェクトとしたものである。手続きオブジェクトとも言う(プロシージャ=手続き)。なお、ここでのオブジェクトの語は、いわゆるオブジェクト指向のそれに限らず、「第一級オブジェクト」という語におけるのと同じ、メモリ上に空間を確保されたもの、といった意味である。関数が第一級オブジェクトである場合は特に第一級関数と言う。

関数と変数の名前空間が共通である言語の場合、構文の設計によっては、a = hoge(b) といったような、通常のサブルーチン呼び出しと全く同じ構文で、関数オブジェクトが意味するサブルーチンを呼び出せる言語もある。

また、変数束縛が閉じられた関数オブジェクトはクロージャである。C#などには関数オブジェクトのようなものとして、オブジェクトのインスタンスとそのメソッドである手続きとを結びつけている、デリゲートがある。無名関数も参照。

用途[編集]

関数オブジェクトの典型的な用途は、より優れたコールバックを記述することである。C言語では、コールバックには関数へのポインタを使う他ないが、コールバックの内外で状態変数を共有できない。この制限のために、関数の動的な振る舞いが制約されてしまう。以下に擬似コードで例を示す。

foo()
{
    // 状態変数
    static int state = 0;
 
    // コールバックとして callback という関数へのポインタを登録
    regist_callback(&callback);
}
 
callback()
{
    ...
    // ここから状態変数 state にアクセスしたくても、不可能
}

これに対し、関数オブジェクトを使うことでFacade パターンを実装し、こうした問題を解決することができる。

LISP や、C++JavaPythonRuby などの現代的なオブジェクト指向言語は、ほとんどが関数オブジェクト、ないし同等の機能をサポートしており、さらに有意義な使い方をしているものもある。

起源[編集]

関数オブジェクトは、LISPにおいてその初期から研究された。計算機プログラムの構造と解釈の第3章でも解説されている。

オブジェクト指向言語では、Smalltalk において、ブロックが関数オブジェクトの記法となるよう設計された。たとえば配列の各要素を通常の大小関係とは違う順序で並べ替えたい場合、比較のための関数オブジェクトを引数に取るソートメソッドを、引数としてブロックを付けて、呼び出す。ブロック内には、カスタマイズ版の比較手続きを記述する。ソートメソッド内での比較は、渡された関数オブジェクトの手続きを呼び出すことで行われ、期待する大小関係でのソートがおこなわれる。これは、strategy デザインパターンの完全な具現化であり、プラガブルな振る舞いを促進するものである。

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++ のオブジェクトを C++ の用語ではファンクタ(functor)と呼ぶ(#C++のファンクタとファンクショノイド を参照)。

class compare_class {
  public:
  bool operator()(int A, int B) const {
    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++のファンクタとファンクショノイド[編集]

以上のように 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 関数 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;
}

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(
      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.lang.Runnable が挙げられる。コーディングにおいては、そのようなインタフェースを実装した無名インナークラスがしばしば使われる。

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

一方で Python には、その内部に式しか書くことができない(文が書けない)という強い制限のある lambda form しか関数リテラルに相当するものがなく、手続き的なものは名前を付けて定義しなければならない。これはそうするのが良いプラクティスだからとされている。

Lisp における関数オブジェクト[編集]

Lisp においても、関数は、文字列やベクトル・リスト・数値と同様に変数に入れたり関数から返したりできる第一級オブジェクトであり、第一級関数を扱うことができる言語である。

Lisp はその最初から (lambda ...) という特殊形式による関数リテラルをはじめとして、第一級関数を扱うことができる言語であったが、1960年代から1970年前後までの実装では、動的スコープのためクロージャにはなっておらず、funarg問題(en:Funarg problem)が認識されることとなった。Scheme で、静的スコープと無限エクステント(メモリ上のオブジェクトは参照されている限り生存し続ける)による解決が示され、Common Lisp をはじめとする現代的な Lisp の多くは静的スコープを採用しており、lambda 特殊形式ではクロージャが作られる。しかし Emacs 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 という変数に関数値を束縛する所では、通常の文脈において関数にアクセスするために #' を名前の前に付けている。一方、並びの先頭の位置に、この例では F と書いても、その名前の関数がないというエラーになる。Common Lisp では一般に、FUNCALL か APPLY 関数を使って呼び出す。

変数と関数の名前空間を分けるか同じにするか、という議論は、Lisp に限らず現代的なプログラミング言語の設計において話題になる。ML などの現代的な関数型言語では当然のように同じ名前空間である。スクリプティング言語では、Python や JavaScript は同じ名前空間としたが、Ruby ではローカル変数とメソッドで別の名前空間とした。Lisp では「Lisp-1 対 Lisp-2 の議論」などと呼ばれる(en:Common Lisp#The function namespace を参照)。

Ruby における関数オブジェクト[編集]

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 は、シンプルなハックを開発した

class Symbol
   def to_proc
      proc { |obj, *args| obj.send(self, *args) }
   end
end

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 に追加された[2]

また、Ruby においてファンクタという名前があるものとして、Ruby Facets プロジェクトによって導入された委譲の実装がある。委譲の最も基本的な定義は下記のようなものである:

class Functor
  def initialize(&func)
    @func = func
  end
  def method_missing(op, *args, &blk)
    @func.call(op, *args, &blk)
  end
end

ファンクタ[編集]

ここで説明するのは 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

Prologでは、関数のシンボルをファンクタと呼んでいる。

文献[編集]

参考[編集]

  1. ^ C++ FAQ [33.15] What's the difference between a functionoid and a functor?

外部リンク[編集]