C# のクロージャと部分適用とカリー化

 C#クロージャや部分適用やカリー化を使いこなすためのメモ。

クロージャ

 英語のスペルでは「closure」、日本語で言い直すと「閉包」。
 クロージャの特徴を掻い摘んで書くと、ローカル変数等をキャプチャーしたレキシカル変数を持ち、その値を操作・参照するラムダ式を返す、というものです。
 つまり、どんなものかを手っ取り早くクロージャの例を示してみると、

Func<int, Func<int>> method = x =>
{
    var captured = x;
    return () => ++captured;
};
var increment = method(100);

Console.WriteLine(increment());
Console.WriteLine(increment());

となります。これを実行すると、以下のような結果が出力されます。

101
102

 さて、クロージャを理解するうえでの勘所は、レキシカル変数となります。このレキシカル変数は、外側のラムダ式(エンクロージャと呼ぶ)を呼び出した時点で値が確保され、そのエンクロージャを有効範囲とする変数です。上記例で言えば、エンクロージャ method の引数に 100 を渡した時点で、レキシカル変数 captured に 100 が代入されます。
 レキシカル変数がエンクロージャを有効範囲とするメリットは、上記例の8行目と9行目に活きてきます。なぜならラムダ式 increment を呼び出すたびに、エンクロージャにある実体が同一の変数 captured に対しての操作が行われるからです。従って、captured がインクリメントされた値が上記のような結果として出力されるのです。面白いですね。
 ちなみに、エンクロージャ method の仮引数 x もレキシカル変数扱いです。ですので、例の1行目から4行目は、

Func<int, Func<int>> method = x => () => ++x;

と、もっと簡単に1行で書けてしまいます。加えて、例の1行目から5行目まではよりコンパクトに、

var increment = ((Func<int, Func<int>>)(x => () => ++x))(100);

と書くこともできますが、エンクロージャ部分が簡単なものであっても、あとから見直した時に訳が分からなくなりかねないので、積極的な利用は避けた方が得策です。

部分適用とカリー化

 クロージャを応用したものが、 部分適用とカリー化です。
 部分適用とカリー化も、エンクロージャが返すラムダ式の引数を減らすという目的があり、部分適用は、1つ以上引数を減らしたものに対して用い、一方、カリー化は、減らした後の引数が1つだけになるものに対して用います。つまり、カリー化は部分適用に含まれます。すなわち、部分適用のうち、減らした後の引数が1つだけになるものがカリー化なのです。

部分適用

ソースコード

Func<int, Func<int, int, int>> method = x => (y, z) => x + y + z;
var multiPlus = method(100);

Console.WriteLine(multiPlus(100, 1));
Console.WriteLine(multiPlus(200, 2));

実行結果

201
302

 やっていることの基本は、クロージャと変わりありません。わずかな違いと言えば、エンクロージャが返すラムダ式の仮引数が一つ以上ある、ということくらいです。
 部分適用は、予め使う値が決まっている変数をエンクロージャの引数とし、レキシカル変数としてキャプチャしておいて、動的に変更される一つ以上の変数をエンクロージャが返すラムダ式の引数として実行時に渡すことができるメリットがあります。
 上記例はあまり良い例とは言えませんが、部分適用は、呼び出しごとに値が変わらない変数を引数として渡す必要があるメソッドを置き換えることでき、それにより可読性向上の効果が期待できます。

カリー化

ソースコード

Func<int, Func<int, int>> method = x => y => Math.Pow(x, y);
var twoPower = method(2);

Console.WriteLine(twoPower(2));
Console.WriteLine(twoPower(3));

実行結果

4
8

 部分適用と実質同じなので特に言うことはないです。部分適用に比べてカリー化を使う機会は、カリー化自体の特性からあまりないかもしれません。一つ以外の引数の値が予め決められるという機会に恵まれることは稀有でしょうから。

まとめ

 以下、要点。

  • クロージャの概要
    クロージャとは、ローカル変数等をキャプチャーしたレキシカル変数を持ち、その値を操作・参照するラムダ式を返すもの。
  • クロージャの要点
    クロージャを使いこなすための肝となるのは、レキシカル変数の扱い方・扱われ方について覚えること。
  • 部分適用とカリー化の違い
    エンクロージャが返すラムダ式の仮引数を減らす部分適用のうち、仮引数が一つだけになるものが特別にカリー化と呼ばれる。

 クロージャも部分適用もカリー化も上手く使いこなせれば、オブジェクト指向パラダイムに縛られた思考から解放されます。ただ、これらを使う際に、他の言語に比べてC#ではラムダ式の宣言が長くなりがちで、可読性が落ちやすくコーディング量がちょっとばかり多めな部分が唯一挙げるデメリットかな、と思います。