Rubyでクロージャ、カリー化
(2006.1.28の日記を修正、加筆。)
(2006.10.8 新規公開。)
Rubyの生産性の高さの理由のひとつクロージャ (closure; 閉包) と、関数・クロージャの単位で再利用できるようにするカリー化について。
コールバック関数
Rubyの話のまえに、クロージャのないC/C++で関数を再利用する方法、その問題点を見ていく。
まずは、単純なコールバックについて。コールバックといえばqsort()関数。引数として二つの値を比較する関数を渡す。
2| #include <stdlib.h> 3| #include <stdio.h> 4| 5| int compare(const int* a, const int* b) { 6| return *a - *b; 7| } 8| 9| int main() { 10| int a[] = {3, 4, 1, 2}; 11| int i; 12| 13| qsort(a, sizeof(a) / sizeof(int), sizeof(int), compare); 14| 15| for (i = 0; i < 4; i++) printf("%d ", a[i]); 16| printf("\n"); 17| 18| return 0; 19| }
qsort() のプロトタイプ宣言は、下記のようになっているので、コールバック関数の型が合わない警告が出る。コールバック関数の中でキャストしてもいいが、面倒。
void qsort(void *base, size_t nmemb, size_t size,
int(*compar)(const void *, const void *));
また、コールバック関数をカスタマイズして再利用できない。
関数オブジェクト (ファンクタ)
C++では、関数オブジェクト(ファンクタ/functor; 関手)がある。
関数オブジェクトは、関数呼び出し(operator () メソッド)を持つオブジェクトで、関数のように扱える。オブジェクトなので、内部状態を持つことができる。
下記の例は、関数オブジェクトのクラスIfModZeroを定義している。コンストラクタで引数として底を取り、operator () ではその底を使う。これで、底が何であってもこのクラスを使える。
2| #include <vector> 3| #include <algorithm> 4| #include <iostream> 5| 6| using namespace std; 7| 8| struct IfModZero: public unary_function<int, bool> { 9| int base; 10| IfModZero(int v): base(v) {} 11| bool operator() (int x) const { 12| return (x % base == 0) ? true : false; 13| } 14| }; 15| 16| int main() { 17| int ary[] = {1, 2, 3, 4}; 18| int out[10]; 19| 20| int* r = remove_copy_if(ary, ary + 4, out, unary_negate<IfModZero>(IfModZero(2))); 21| for (int* p = out; p != r; p++) 22| cout << *p << " "; 23| cout << "\n"; 24| return 0; 25| }
C++のアルゴリズムライブラリ(<algorithm>ヘッダ)は、for_each, find_ifなどを提供している。上記サンプルのremove_copy_if() もその一つ。下記は、区間 [first, last) において値を検索するfind_ifと、区間 [first1, last1) が [first2...) と一致するか調べるequalのプロトタイプ宣言。
template<class InputIterator, class Predicate>
InputIterator find_if(InputIterator first, InputIterator last,
Predicate pred);
template
<class InputIterator1, class InputIterator2, class BinaryPredicate>
bool equal(InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, BinaryPredicate pred);
Predicate型には、unary_function<T, bool>の派生クラスで、
bool operator()(const T& x) constメソッドを持つオブジェクトを与え、BinaryPredicate型には二つの引数を取る関数呼び出しを持つオブジェクトを与える。
C++は、コールバック関数をカスタマイズすることで、再利用可能なようになっている。しかし、いちいちクラスを書くのは相当めんどくさい。
クロージャ
クロージャ (closure, 閉包) は、C++でのファンクタを簡潔にしたもの。また、関数内部から外側の変数にアクセスできる。
Note.
|
Rubyのクロージャ(ブロック)は、ラムダ式 (lambda) によって生成され、ファンクタと同様、オブジェクトとして扱える。関数呼び出しはcallメソッド。さらに、関数・メソッドの引数としてクロージャを渡す場合、構文
メソッド(引数, ...) {|引数| ...}
でクロージャを生成し、自動的にブロック引数として渡される。Rubyでは、後者の構文によるクロージャの生成を多用する。
2| ary = [1, 2, 3, 4] 3| p ary.select {|x| x % 2 == 0} #=> [2, 4] 4| 5| result = 0 # 外側の変数 6| (1..10).each {|x| result += x} 7| p result #=> 55
Rubyでカリー化
メソッド・関数・ラムダオブジェクトをカリー (curry) 化する。
ある関数があったとして、この関数の仮引数の一部をあらかじめ指定する値で固定し、新しい関数オブジェクトを生成する。新しい関数オブジェクトは引数の数が少なくなる。関数の部分適用ともいう。
重要.
|
関数の呼び出しでコールバック関数を渡したいが、渡す関数の引数の数を減らしたいときに使う。
Rubyでは、関数っぽいものとしてラムダ (Procオブジェクト)、メソッド (Methodオブジェクト) もあるので、これらもカリー化できるようにしてみる。
2| module Currying 3| # カリー化する 4| # 実引数の数がメソッド・関数の仮引数の数と同じか多ければ 5| # 関数を呼び出し、少なければ関数を生成して返す。 6| # 実引数は左側から詰める。 7| def currying(*args) 8| ari = arity >= 0 ? arity : -arity - 1 9| if ari <= args.length 10| call(*args) 11| else 12| lambda {|*a| currying(*(args + a))} 13| end 14| end 15| end 16| 17| class Proc 18| include Currying 19| end 20| 21| class Method 22| include Currying 23| end
関数のテスト。bigger_than.currying(12)とは書けない。method関数でメソッドオブジェクトを得てからカリー化する。
27| def bigger_than(val, x) 28| x > val ? true : false 29| end 30| 31| f = method(:bigger_than).currying(12) 32| # bigger_than.currying(12) はエラー 33| 34| p [11, 12, 13].find_all(&f) # ブロックとして与える。&を省略するとエラー 35| #=> [13]
次はラムダ。
39| lam = lambda {|val, x| x > val ? true : false}
40|
41| f = lam.currying(12)
42| p [11, 12, 13].find_all(&f) #=> [13]
43| p [12, 30].find_all(&lam.currying(12)) #=> [30]
最後はメソッド。レシーバオブジェクトが特定されていなければならない。
48| class Foo 49| def hoge(x, y) 50| x.to_s + y.to_s 51| end 52| end 53| 54| v = Foo.new.method(:hoge).currying('fuga') 55| p v.call('hogehoge') #=> "fugahogehoge"
まとめ
カリー化によって、関数・メソッド単位をカスタマイズして再利用できるようになる。引数は、左側にカスタマイズのパラメータとなるような項目を持ってくるのがいい。
サイト内関連ページ
- Haskellで遊ぶ
- Rubyで無理をしなくたって、Haskellなら関数単位で再利用できる。
Note.
重要.