コンテンツにスキップ

関数オブジェクト

出典: フリー百科事典『ウィキペディア(Wikipedia)』

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

関数と変数の名前空間が共通である言語の場合[要追加記述]、構文の設計によっては、y = f(x) といったような、通常のサブルーチン呼び出しと全く同じ構文で、関数オブジェクトが保持しているサブルーチンを呼び出せる言語もある。一方、通常のサブルーチンのように呼び出すことはできず、applyacceptgetといった特別な名前のメソッドを経由して呼び出す必要のある言語もある。

また、変数束縛が閉じられた関数オブジェクトはクロージャである。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や、SmalltalkC++JavaC#PerlPythonRubyなどの現代的なオブジェクト指向言語は、ほとんどが関数オブジェクト、あるいは同等の機能をサポートしており、さらに有意義な使い方をしているものもある[要追加記述]

起源

[編集]

関数オブジェクトは、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++ では別の種類の関数オブジェクトが可能である。[要校閲]

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 関数 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[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&)>() の場合にはコンパイル時に関数が不定でありインライン化できない。

現実には、コンパイラに指示すれば簡単に関数を既知にすることができる。コンパイラが関数の定義を認識しており、それがクラスの内外いずれでも同じように行われていさえすればよい。インライン化しない場合、リンカは関数がクラスの関数だとの指示さえあれば同じ関数の別のコンパイル単位の複数回の定義をエラーを生成せずに黙って見過ごす。リンカは同じ関数の定義がクラスの関数でない場合には複数回の定義を許容しないためである。

もうひとつの例として、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では関数が第一級オブジェクトでないため、関数オブジェクトの代わりとして、一つのメソッドを持つインタフェースが使われる[注釈 1]。代表的なインタフェースとしては、run()メソッドを持つjava.lang.Runnableが挙げられる。実際のコーディングにおいては、そのようなインタフェースを実装するクラスの定義には、内部クラス (inner class) や、メソッド内の変数をキャプチャしてクロージャのように振る舞うことのできるローカルクラス (local class) あるいは匿名クラス (anonymous class) がしばしば使われる。

Javaの標準ライブラリの例では、java.util.Collections.sort(List, Comparator) はリストとコンパレータ(意味的には関数)を引数にとる。第2引数のコンパレータはリスト内のオブジェクトを比較する役割を持つ。しかし、Javaは関数が第一級オブジェクトでないため、java.util.Comparatorインタフェースを実装したオブジェクトを渡す。下記の例のように使用する:

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を使用することもできるようになった[2]

また、Java 8はメソッド参照もサポートするようになった。メソッド参照は、内部的にはインタフェースを利用して実装されている。

java.util.functionパッケージには、ラムダ式やメソッド参照のターゲットとして利用可能な汎用の関数型インタフェースがいくつか定義されており、これらを利用すればユーザーコードでインタフェースを明示的に定義する手間を省くことができる。

Python における関数オブジェクト

[編集]

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 というインスタンスメソッドにより呼ぶことができる[3]

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

Smalltalkにおける関数オブジェクト

[編集]

冒頭にも述べたようSmalltalkでは関数オブジェクトに類似する機能としてブロックが存在する。他の言語同様関数的な使い方も可能であるが、複数の実行方法を備えており多機能になっている。Smalltalkでは言語構文としてif文を始め殆どの制御構文が存在せず、メッセージとこのブロックの組み合わせによってあらゆる制御を実現しておりブロックはSmalltalkの根幹をなしている。

下記に一般的なブロックの使用例を示す。[から]までがブロックである。

Number methodsFor:'comparing'
!
max: aNumber
    "レシーバーと引数を比較して大きい方を返す。"
    self < aNumber
        ifTrue:
        [
            ^ aNumber. "ブロック内ではブロックの外側にある変数も参照できる"
        ]
        ifFalse:
        [
            ^ self.
        ].
!!

上記における^は他の言語のreturnに当たる復帰文であるが、この復帰文はブロックの呼び出し階層を無視しブロックを含むメソッドの呼び出し元まで一気に戻るという特殊な働きをしており、更に他の言語と一線を隔てる特徴となっている。この復帰文によって、ブロックはより制御構文としての性格が強くなっている。

ファンクタ

[編集]

ここで説明するのは 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. ^ 匿名クラスの構文であれば、実際にはインタフェースだけでなく任意の具象クラスや抽象クラスを継承することもできるが、Javaでは実装(クラス)の多重継承ができず、クラスを使うと自由度が低下するため、一般的に関数オブジェクトとして使われるのはインタフェースである。

出典

[編集]
  1. ^ C++ FAQ [33.15] What's the difference between a functionoid and a functor?
  2. ^ イマドキのJava徹底入門(13) ラムダ式の入力パラメータにvarを使用する | TECH+(テックプラス)
  3. ^ スクリプトを記述する上では簡単なように設計されているが、内部ではわりとコストが掛かる。ブロックはエスケープしないため軽い処理で扱うことができるし、元々イテレータのためのものだったため、繰り返し処理のために使われることからも、軽いほうが望ましい。それがオブジェクトになれば、メソッドの返り値などになり得るために、重い処理となるためである。

外部リンク

[編集]