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