ようこそ。睡眠不足なプログラマのチラ裏です。

C#2.0の匿名メソッドによるカリー化

関数プログラミングとカリー化(引数束縛)

この技法は、論理学者ハスケル・カリーにちなんで名付けられた。


カリー化の起源は、関数の数学的研究にある。
複数の引数を持つ関数について、任意の引数を固定(束縛)し、残った引数を持つ関数を取得しようという考え方で、
引数を1つ持つ高階関数*1を複数組み合わせることで実現される。
つまり、C#3.0のラムダ式でいうところの、「(x, y) => z)」をカリー化すると 「(x => (y => z))」となる。
また、このとき‘x’はデカルト積(直積集合)であり、ラムダ演算子‘=>’*2は関数空間を指している。
関数プログラミングでは、関数の適用は並置*3によって記述され、右から左へと連鎖的に関連付けられる。


関数をカリー化すると何がうれしいの?
カリー化をする目的と動機というのは、
関数に引数を一部だけ渡して新たな関数を作るのが便利な場合が頻繁にあるからである。
例えば、2つの引数を取り互いを加算する関数の最初の引数を‘1’にしてカリー化を行えば、
再利用可能なインクリメント用の関数が出来上がる。
この例の場合、実用的ではないのであんまりうれしくない気がするが、
カリー化関数は、部分的に引数を束縛することで、条件に応じた中間的な関数を容易にいくつでも作ることができる。
引数が1つだとほかの引数の返却を待たずに、おのおのを独立して実行できるので処理の並列化が期待できる。
これは大変便利である。わかり難ければ、オーバーロードを量産できるようなイメージをするとピンとくるかもしれない。


ただし、カリー化は高階関数を用いるので、手続き型言語圏のプログラマにとっては、
少々コードが複雑になってしまいがちという欠点がある。


C#2.0匿名メソッドによるカリー化
C#3.0でカリー化を表現する場合、ラムダ式(lambda)と型推論が使えるので、
非常にすっきりしたかたちで表現することができるが、
C#2.0でカリー化を表現しようとすると、型推論ができず、
高階関数を容易に記述できるラムダ式が使えないので、少々冗長的になってしまう。


以下、C#2.0匿名メソッドによるカリー化サンプル

using System;
using System.Collections.Generic;
using System.Text;

namespace ClassLibrary1
{
    public static class Curry
    {
        public static GenericCurryFunctions.CurriedFunc<T0, T1, TResult> Create<T0, T1, TResult>
        (GenericCurryFunctions.Func<T0, T1, TResult> f)
        {
            return delegate(T0 p0)
            {
                return delegate(T1 p1)
                {
                    return f(p0, p1);
                };
            };
        }

        public static GenericCurryFunctions.CurriedFunc<T0, T1, T2, TResult> Create<T0, T1, T2, TResult>
        (GenericCurryFunctions.Func<T0, T1, T2, TResult> f)
        {
            return delegate(T0 p0)
            {
                return delegate(T1 p1)
                {
                    return delegate(T2 p2)
                    {
                        return f(p0, p1, p2);
                    };
                };
            };
        }
    }

    namespace GenericCurryFunctions
    {
        public delegate TResult Func<T0, TResult>(T0 p0);
        public delegate TResult Func<T0, T1, TResult>(T0 p0, T1 p1);
        public delegate TResult Func<T0, T1, T2, TResult>(T0 p0, T1 p1, T2 p2);

        public delegate Func<T1, TResult> CurriedFunc<T0, T1, TResult>(T0 p0);
        public delegate CurriedFunc<T1, T2, TResult> CurriedFunc<T0, T1, T2, TResult>(T0 p0);
    }
}
using System;
using System.Collections.Generic;
using System.Text;
using ClassLibrary1;
using ClassLibrary1.GenericCurryFunctions;

namespace ConsoleApplication1
{
    class Program
    {
        [STAThread]
        static void Main()
        {
            CurriedFunc<int, int, int, int> cf = Curry.Create<int, int, int, int>(TestMethod);
            CurriedFunc<int, int, int> testCurry1 = cf(3);
            Func<int, int> testCurry2 = cf(3)(5);

            Console.WriteLine(testCurry1(3)(2));
            Console.WriteLine(testCurry2(3));
            Console.Read();

        }

        public static int TestMethod(int x, int y, int z)
        {
            return x * (y + z);
        }
    }
}

結果:
15
24

*1:関数を引数や戻り値とする関数。

*2:「〜に入力」と読む

*3:関数の並置記述とは、無関係な関数を並べ、それが複合関数のようになって新しい意味の関数を作ることを指します。