C#でカリー化と部分適用

勉強会で話題になっておもしろかったので、実装してみました。

カリー化

4つの引数まで対応したFuncの拡張メソッド。

    public static class FuncCurry
    {
        public static Func<T1, Func<T2, TResult>> Curry<T1, T2, TResult>(this Func<T1, T2, TResult> f)
        {
            return arg1 => arg2 => f.Invoke(arg1, arg2);
        }

        public static Func<T1, Func<T2, Func<T3, TResult>>> Curry<T1, T2, T3, TResult>(this Func<T1, T2, T3, TResult> f)
        {
            return arg1 => arg2 => arg3 => f.Invoke(arg1, arg2, arg3);
        }

        public static Func<T1, Func<T2, Func<T3, Func<T4, TResult>>>> Curry<T1, T2, T3, T4, TResult>(this Func<T1, T2, T3, T4, TResult> f)
        {
            return arg1 => arg2 => arg3 => arg4 => f.Invoke(arg1, arg2, arg3, arg4);
        }	
    }

テストコード。

    [TestClass]
    public class FuncCurryTest
    {

        [TestMethod]
        public void Test2Args()
        {
            Func<int, int, int> add = (a, b) => a + b;
            Assert.AreEqual(3, add(1, 2));

            var f = add.Curry();
            Assert.AreEqual(3, f(1)(2));
        }

        [TestMethod]
        public void Test3Args()
        {
            Func<int, int, int, int> add = (a, b, c) => a + b + c;
            Assert.AreEqual(6, add(1, 2, 3));

            var f = add.Curry();
            Assert.AreEqual(6, f(1)(2)(3));
        }

        [TestMethod]
        public void Test4Args()
        {
            Func<int, int, int, int, int> add = (a, b, c, d) => a + b + c + d;
            Assert.AreEqual(10, add(1, 2, 3, 4));

            var f = add.Curry();
            Assert.AreEqual(10, f(1)(2)(3)(4));
        }
    }

通常の呼び出し方と比べると、違いは一目瞭然ですね。
拡張メソッドのCurryを呼び出すと、1つの引数を受け取る関数をチェーンできます。

部分適用

4つの引数まで対応したFuncの拡張メソッド。

    public static class FuncPartial
    {
        public static Func<T2, TResult> Partial<T1, T2, TResult>(this Func<T1, T2, TResult> f, T1 arg1)
        {
            return arg2 => f.Invoke(arg1, arg2);
        }

        public static Func<T3, TResult> Partial<T1, T2, T3, TResult>(this Func<T1, T2, T3, TResult> f, T1 arg1, T2 arg2)
        {
            return arg3 => f.Invoke(arg1, arg2, arg3);
        }

        public static Func<T2, T3, TResult> Partial<T1, T2, T3, TResult>(this Func<T1, T2, T3, TResult> f, T1 arg1)
        {
            return (arg2, arg3) => f.Invoke(arg1, arg2, arg3);
        }

        public static Func<T4, TResult> Partial<T1, T2, T3, T4, TResult>(this Func<T1, T2, T3, T4, TResult> f, T1 arg1, T2 arg2, T3 arg3)
        {
            return arg4 => f.Invoke(arg1, arg2, arg3, arg4);
        }

        public static Func<T3, T4, TResult> Partial<T1, T2, T3, T4, TResult>(this Func<T1, T2, T3, T4, TResult> f, T1 arg1, T2 arg2)
        {
            return (arg3, arg4) => f.Invoke(arg1, arg2, arg3, arg4);
        }

        public static Func<T2, T3, T4, TResult> Partial<T1, T2, T3, T4, TResult>(this Func<T1, T2, T3, T4, TResult> f, T1 arg1)
        {
            return (arg2, arg3, arg4) => f.Invoke(arg1, arg2, arg3, arg4);
        }
    }

テストコード。

    [TestClass]
    public class FuncPartialTest
    {
        [TestMethod]
        public void Test2Args()
        {
            Func<int, int, int> add = (a, b) => a + b;
            Assert.AreEqual(3, add(1, 2));

            var add1 = add.Partial(1);
            Assert.AreEqual(3, add1(2));
        }

        [TestMethod]
        public void Test3Args()
        {
            Func<int, int, int, int> add = (a, b, c) => a + b + c;
            Assert.AreEqual(6, add(1, 2, 3));

            var add1 = add.Partial(1);
            Assert.AreEqual(6, add1(2, 3));

            var add12 = add.Partial(1, 2);
            Assert.AreEqual(6, add12(3));
        }

        [TestMethod]
        public void Test4Args()
        {
            Func<int, int, int, int, int> add = (a, b, c, d) => a + b + c + d;
            Assert.AreEqual(10, add(1, 2, 3, 4));

            var add1 = add.Partial(1);
            Assert.AreEqual(10, add1(2, 3, 4));

            var add12 = add.Partial(1, 2);
            Assert.AreEqual(10, add12(3, 4));

            var add123 = add.Partial(1, 2, 3);
            Assert.AreEqual(10, add123(4));
        }
    }

拡張メソッドのPartialで引数の一部を渡すと、残りの引数を受け取る関数を返します。

部分適用(引数入れ替え)

第1引数と第2引数を入れ替えるFuncの拡張メソッド。

    public static class FuncFlip
    {
        public static Func<T2, T1, TResult> Flip<T1, T2, TResult>(this Func<T1, T2, TResult> f)
        {
            return (arg1, arg2) => f.Invoke(arg2, arg1);
        }
    }

部分適用と組み合わせたテストコード。

    [TestClass]
    public class FuncFlipTest
    {
        [TestMethod]
        public void TestFlip()
        {
            Func<decimal, decimal, decimal> div = (a, b) => a / b;

            var f = div.Partial(2M);
            Assert.AreEqual(0.2M, f(10M)); // 2 / 10

            var g = div.Flip().Partial(2M);
            Assert.AreEqual(5M, g(10M));   // 10 / 2
        }
    }

まとめ

カリー化と部分適用は混同しがちですが、違うものですね。
カリー化は、使いどころをあんまり思い浮かばないのですが、部分適用は便利な場合がそこそこあると思います。