新しいスタイル-環境を持ち運ぶ
かなり駆け足でしたがC# 3.0の仕様をざっと紹介しました。ここからが本題です。
今から紹介するテクニックは関数型言語や動的型付言語を学んだことのある方には少々退屈かもしれません。また、C# 2.0でも実現可能なものもあります(しかし、C# 3.0ではよりエレガントに記述できます)。なお、コードは簡略化したいためエラー処理等を省いています。
クロージャ(関数閉包)
まず手始めに、C# 3.0で単純なクロージャを書いてみたいと思います。他言語でのクロージャの説明で度々出現する謎の加算機(!)を作ります。
public void Foo() { Func<int,Func<int,int>> generator = ( (x) => ((y) => x + y) ); var add10 = generator( 10 ); var add50 = generator( 50 ); Console.WriteLine( add10( 10 ) ); Console.WriteLine( add50( 10 ) ); }
20 60
generatorは関数を返す関数です。 generatorの呼び出しにより返される(内側の)関数は、generatorの引数であるxも計算に使っています。実際add10やadd50には内側の関数が代入されますが、クロージャ作成時の外側の関数の状態(環境という)をも保持し続けています。クロージャという言葉は説明する人により若干違いはありますが、一般にはこのような関数と環境のペアのことをクロージャと呼んでいます。イメージ的には環境を関数内に閉じ込めているため、クロージャは別名「関数閉包」と呼ばれたりもします。ちなみに今回のような、関数を受け取ったり返したりする関数を「高階関数」と言います。
書き方の違いでしかありませんが、括弧を減らすと
generator = ( x => (y => x + y) );
という感じになり、さらに減らすと、
generator = x => y => x + y;
となります。
(setf generator
#'(lambda (x)
#'(lambda (y) (+ x y)) ))
(setf add10 (funcall generator 10))
(funcall add10 10)
=>20
さて、いきなりクロージャを示してみたものの、クロージャ初体験の方には少々分かりづらかったかもしれません。C# 2.0の匿名メソッドでも環境を持ち運ぶことが可能でしたが、その点からすると人によっては次の例の方が直感的かもしれません。やっていることは同じです。
public void Foo2() { var add10 = this.CreateClosure( 10 ); //generator( 10 ); var add50 = this.CreateClosure( 50 ); //generator( 50 ); Console.WriteLine( add10( 10 ) ); Console.WriteLine( add50( 10 ) ); } private Func<int, int> CreateClosure( int x ) { return ( y => x + y ); }
まだしっくり来ない方は、Reflector等で逆アセンブルして展開後のコードを参照していただくと理解が深まるかと思います。
もう少し別の例も見てみましょう。 この例では型の情報をクロージャに保存しています。
public class HogeA { public void Hello() { Console.WriteLine( "hello" ); } } public class MainClass { public void MainFunction() { var f = Foo<HogeA>(); var instance = f(); instance.Hello(); } private Func<T> Foo<T>() where T : new() { return ( () => new T() ); } }
fに代入されるのは型情報を保持した関数で、fを呼び出すことによって実際にインスタンスを生成します。
引数のあるコンストラクタに対応するにはもう少し工夫が必要ですが、一時的に型情報を保持しておいて、必要になった時にインスタンスを作成したい!といった時に便利かもしれません。
次はちょっと発展して、キャッシュ機能を持ったクロージャを作ってみます。
public void Foo() { var c = this.CreateClosure( 10, 20 ); Console.WriteLine( c() ); Console.WriteLine( c() ); Console.WriteLine( c() ); } private Func<int> CreateClosure( int a, int b ) { int? result = null; return () => { if ( result == null ) { result = a * b; Console.WriteLine( "!!" ); } return result.Value; }; }
!! 200 200 200
結果から分かるように、3回の呼び出しに対してビックリしてるのは1回だけです。例では単純な掛け算を行っているだけなので恩恵はありませんが、計算量が多い処理(かつ結果が不変)である場合は有用です。また、呼び出す側はその関数がキャッシュ機能を持っているかどうかを意識せず、同じように使うことができます。
今示したのは単純な例ですが、クロージャが引数を受け取るような場合はハッシュテーブルなどを用いて、引数の値と結果のペアを記憶しておく必要があります。
最後に簡単な遅延評価の例を見てみましょう。遅延評価とはまさに「評価を遅らせる」という概念です。「せっかく評価しても値が使われない可能性があるなら、実際に値が必要になった時に計算しようぜ!」というのが遅延評価の基本的な考え方です(用途はこれだけではありませんが、ここでは割愛します。また、厳密に言うと以下の例は完全な遅延評価機能ではありませんが、ここでは概念を説明するに留めておきます)。
早速例に示します。最初は遅延評価を用いていない例です。
public void MainFunction() { int x = 2; int y = 5; int z = 9; int result = this.Foo( false, (x * y), z ); Console.WriteLine( result ); } private int Foo( bool flag, int value1, int value2 ) { if ( flag ) return value1 * value2; return value2 + 1; }
特に意味も無い計算ですが、flagの値がfalseの時は、value1の値が利用されていません。しかし、Foo関数を呼び出す時点でx * yの演算が行われてしまっています。これではもったいないお化けの逆鱗に触れてしまいます。
次に示す例は遅延評価を取り入れたものです。
public void MainFunction() { int x = 2; int y = 5; int z = 9; int result = this.Foo( false, (() => x * y), z ); Console.WriteLine( result ); } private int Foo( bool flag, Func<int> fn, int value ) { if ( flag ) return fn() * value; return value + 1; }
実行結果は先ほどの例と変わりませんが、Fooの第2引数が関数を受け取るようになっています。 Fooを呼び出す側ではクロージャを作成してxとyを保持させていますが、ラムダ式はあくまで式であって、この時点ではx * yの演算は行われていません。実際に計算結果が必要になった場所で、関数呼び出しという形で値を取得しています。
ここではクロージャによって計算をワンテンポ遅らせましたが、C# 2.0で追加されたyield returnでも遅延評価を表現できます(岩永さんのページに大変参考になる記事があります)。
