C# ジェネリックメソッドの定義がえぐい
はじめに
C# 続・メソッドチェーンしたくて仕方なかった - さんぽみち とかで使いたい処理が
いっぱい出てきて、作ってた時に思ったメソッド達を作ってたら見た目がひどいものができました。
なのでそれを乗っけてみます。
成果物
まずはこちら。
HighOrderFunctions.cs
namespace HighOrderFunctions { namespace ExtentionMethods { public static class HighOrderFunctionsExtentionMethods { ///<summary>定数を定数メソッドへ変換する拡張メソッド</summary> ///<param name="a1">定数</param> ///<returns>a1を返す定数メソッド</returns> public static Func<T> ToFunc<T>(this T a){ return () => a; } // デリゲートへの部分適用、とりあえず7引数まで public static Func<T2> Apply<T1, T2>(this Func<T1, T2> f, T1 a1) { return () => f(a1); } public static Func<T2, T3> Apply<T1, T2, T3>(this Func<T1, T2, T3> f, T1 a1) { return a2 => f(a1, a2); } public static Func<T2, T3, T4> Apply<T1, T2, T3, T4>(this Func<T1, T2, T3, T4> f, T1 a1) { return (a2, a3) => f(a1, a2, a3); } public static Func<T2, T3, T4, T5> Apply<T1, T2, T3, T4, T5>(this Func<T1, T2, T3, T4, T5> f, T1 a1) { return (a2, a3, a4) => f(a1, a2, a3, a4); } public static Func<T2, T3, T4, T5, T6> Apply<T1, T2, T3, T4, T5, T6>(this Func<T1, T2, T3, T4, T5, T6> f, T1 a1) { return (a2, a3, a4, a5) => f(a1, a2, a3, a4, a5); } public static Func<T2, T3, T4, T5, T6, T7> Apply<T1, T2, T3, T4, T5, T6, T7>(this Func<T1, T2, T3, T4, T5, T6, T7> f, T1 a1) { return (a2, a3, a4, a5, a6) => f(a1, a2, a3, a4, a5, a6); } // デリゲートのカリー化、とりあえず7引数まで public static Func<T1,Func<T2, T3>> Curry<T1, T2, T3>(this Func<T1, T2, T3> f) { return a1 => f.Apply(a1); } public static Func<T1, Func<T2, Func<T3, T4>>> Curry<T1, T2, T3, T4>(this Func<T1, T2, T3, T4> f) { return a1 => a2 => f.Apply(a1).Apply(a2); } public static Func<T1, Func<T2, Func<T3, Func<T4, T5>>>> Curry<T1, T2, T3, T4, T5>(this Func<T1, T2, T3, T4, T5> f) { return a1 => a2 => a3 => f.Apply(a1).Apply(a2).Apply(a3); } public static Func<T1, Func<T2, Func<T3, Func<T4, Func<T5, T6>>>>> Curry<T1, T2, T3, T4, T5, T6>(this Func<T1, T2, T3, T4, T5, T6> f) { return a1 => a2 => a3 => a4 => f.Apply(a1).Apply(a2).Apply(a3).Apply(a4); } public static Func<T1, Func<T2, Func<T3, Func<T4, Func<T5, Func<T6, T7>>>>>> Curry<T1, T2, T3, T4, T5, T6, T7>(this Func<T1, T2, T3, T4, T5, T6, T7> f) { return a1 => a2 => a3 => a4 => a5 => f.Apply(a1).Apply(a2).Apply(a3).Apply(a4).Apply(a5); } // 関数の実行を遅延させる public static Func<T2> Delay<T1, T2>(this Func<T1, T2> f, T1 a1) { return () => f(a1); } public static Func<T3> Delay<T1, T2, T3>(this Func<T1, T2, T3> f, T1 a1, T2 a2) { return () => f(a1, a2); } public static Func<T4> Delay<T1, T2, T3, T4>(this Func<T1, T2, T3, T4> f, T1 a1, T2 a2, T3 a3) { return () => f(a1, a2, a3); } public static Func<T5> Delay<T1, T2, T3, T4, T5>(this Func<T1, T2, T3, T4, T5> f, T1 a1, T2 a2, T3 a3, T4 a4) { return () => f(a1, a2, a3, a4); } public static Func<T6> Delay<T1, T2, T3, T4, T5, T6>(this Func<T1, T2, T3, T4, T5, T6> f, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5) { return () => f(a1, a2, a3, a4, a5); } public static Func<T7> Delay<T1, T2, T3, T4, T5, T6, T7>(this Func<T1, T2, T3, T4, T5, T6, T7> f, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6) { return () => f(a1, a2, a3, a4, a5, a6); } public static Action Delay<T>(this Action<T> f, T a) { return () => f(a); } public static Action Delay<T1, T2>(this Action<T1, T2> f, T1 a1, T2 a2) { return () => f(a1, a2); } public static Action Delay<T1, T2, T3>(this Action<T1, T2, T3> f, T1 a1, T2 a2, T3 a3) { return () => f(a1, a2, a3); } public static Action Delay<T1, T2, T3, T4>(this Action<T1, T2, T3, T4> f, T1 a1, T2 a2, T3 a3, T4 a4) { return () => f(a1, a2, a3, a4); } public static Action Delay<T1, T2, T3, T4, T5>(this Action<T1, T2, T3, T4, T5> f, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5) { return () => f(a1, a2, a3, a4, a5); } public static Action Delay<T1, T2, T3, T4, T5, T6>(this Action<T1, T2, T3, T4, T5, T6> f, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6) { return () => f(a1, a2, a3, a4, a5, a6); } public static Action Delay<T1, T2, T3, T4, T5, T6, T7>(this Action<T1, T2, T3, T4, T5, T6, T7> f, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6, T7 a7) { return () => f(a1, a2, a3, a4, a5, a6, a7); } } } }
…次にこちら。
TupleExtention.cs
using System; namespace TupleExtention { namespace ExtentionMethods { public static class TupleExtentionMethods { // タプルのパターンマッチング public static Tuple<T1, T2> Match<T1, T2>( this Tuple<T1, T2> t, out T1 a1, out T2 a2 ){ a1 = t.Item1; a2 = t.Item2; return t; } public static Tuple<T1, T2, T3> Match<T1, T2, T3>( this Tuple<T1, T2, T3> t, out T1 a1, out T2 a2, out T3 a3 ){ a1 = t.Item1; a2 = t.Item2; a3 = t.Item3; return t; } public static Tuple<T1, T2, T3, T4> Match<T1, T2, T3, T4>( this Tuple<T1, T2, T3, T4> t, out T1 a1, out T2 a2, out T3 a3, out T4 a4 ){ a1 = t.Item1; a2 = t.Item2; a3 = t.Item3; a4 = t.Item4; return t; } public static Tuple<T1, T2, T3, T4, T5> Match<T1, T2, T3, T4, T5>( this Tuple<T1, T2, T3, T4, T5> t, out T1 a1, out T2 a2, out T3 a3, out T4 a4, out T5 a5 ){ a1 = t.Item1; a2 = t.Item2; a3 = t.Item3; a4 = t.Item4; a5 = t.Item5; return t; } public static Tuple<T1, T2, T3, T4, T5, T6> Match<T1, T2, T3, T4, T5, T6>( this Tuple<T1, T2, T3, T4, T5, T6> t, out T1 a1, out T2 a2, out T3 a3, out T4 a4, out T5 a5, out T6 a6 ){ a1 = t.Item1; a2 = t.Item2; a3 = t.Item3; a4 = t.Item4; a5 = t.Item5; a6 = t.Item6; return t; } public static Tuple<T1, T2, T3, T4, T5, T6, T7> Match<T1, T2, T3, T4, T5, T6, T7>( this Tuple<T1, T2, T3, T4, T5, T6, T7> t, out T1 a1, out T2 a2, out T3 a3, out T4 a4, out T5 a5, out T6 a6, out T7 a7 ){ a1 = t.Item1; a2 = t.Item2; a3 = t.Item3; a4 = t.Item4; a5 = t.Item5; a6 = t.Item6; a7 = t.Item7; return t; } } } }
C#でのジェネリックに関するメソッド定義は饒舌し難いほどにやりづらいです!!!
これのテストとか絶対やりたくないです。
標準ライブラリでこれくらいなら定義しておいてくれてもいいのに…
そして実装が終わった後の追い打ちがこれ。
using System; using HighOrderFunctions.ExtentionMethods; class MainApp { [STAThread] public static void Main() { // var sumCurry = Add.Curry(); // <=一番やりたかった形がコンパイルエラー // var sumCurry = // // HighOrderFunctionsExtentionMethods // <=すなわちこれもコンパイルエラー // .Curry(Add); // // var add = Add; // <=これがダメなのが致命的 var sumCurry = ((Func<int, int, int, int>)Add).Curry(); // <=これならOK sumCurry = // HighOrderFunctionsExtentionMethods // <=つまりこれもOK .Curry((Func<int, int, int, int>)Add); // Func<int, int, int, int> add = Add; // <=ということは sumCurry = add.Curry(); // <=これもOK Console.WriteLine(sumCurry(1)(2)(3)); // => 6 } public static int Add(int a, int b, int c) { return a + b + c; } }
使うときもめんどくさい。。。
オーバーロードの兼ね合いがあってメソッドはメソッドグループとして扱われてるのが
問題っぽいと推測。
メソッドグループのインスタンスを変数に落とすことができて、かつメソッドグループから
どのオーバーロードかを選択することができれば解決しそうなのになぁ。
C#6.0ではメソッドをラムダ式で定義できるらしいので、環境ある人はこれができるか
やってみてほしい(丸投げ
一応解説
このままじゃ愚痴だけになっちゃうので、軽くだけ説明を。
Func<T1, T2, .T3, .. , TResult> を使い慣れていない方は、先頭から順にカンマを
接続詞の "と" に置き換えながら読み、最後のカンマだけ "から" と読み、
後ろに "へ変換するデリゲート" とつけて読んでみてください。
つまり、型引数が四つなら "T1 と T2 と T3 からTResult へ変換するデリゲート" となります。
ピンと来てくれればいいなぁ。
Applyメソッド
デリゲートへ部分適用してくれるメソッドです。
例えば、今回のAddメソッドでは Func<int, int, int, int> の形をしたメソッドですね。
これを、第一引数をあらかじめ与えて固定することで Func<int, int, int> に
変換することを部分適用と言います。
やってることのイメージはこんな感じ。
int Add(int a, int b, int c) // 最初は引数三つ { return a + b + c; } // ここへaに1を渡して固定したい // イメージ int Add(int b, int c) // 引数は二つで { static int a = 1; return a + b + c; } // 前に渡した1を覚えておきたい // C#で正しく書くと Func<int, int, int> AddApply(int a) // 返すのはデリゲート { return (x, y) => Add(a, x, y); } // クロージャがaを覚えていてくれる // 使ってみると Func<int, int, int> addAppied = Add(1); // (x, y) => 1 + x + y となる int result = addApplied(2, 3); // (2, 3) => 1 + 2 + 3 となるイメージ
身につくとかゆいところに手が届いて便利です。
Curryメソッド
カリー(Curry)化してくれるメソッドです。
こちらもAddメソッドを使って説明すると、Func<int, int, int, int> から
Func<int, Func<int, Func<int, int>>> のような
形に変換することをカリー化と呼びます。おいしそうです。
どういうことかというと、上の例であれば、Func<int, int> の形を T と置くと
Func<int, Func<int, T>>
Func<int, T> を U と置くと
Func<int, U>
という形をしており、常に引数が一つのメソッドとなっています。
元の引数をもっと増やしてみて
Func<int, int ,int, int, int, int, int>
をカリー化すると
Func<int, Func<int, Func<int, Func<int, Func<int, int>>>>>
という形になりますが、どの段階のメソッドを見ても引数が一つになっています。
これで嬉しいのが、先ほどの部分適用を考えたときです。
Func<int, int, int, int>> へ値を渡すと Func<int, Func<int, Func<int, int>>> が返ってきて、
さっきの部分適用と同等なものが実現できています。
逆に言い換えると、カリー化されたメソッドは実行時に部分適用を繰り返して
元のメソッドと等価な演算を行っているともいえます。
また、ジェネリック型のメソッドを設計するときにも嬉しいことがあります。
それは、引数が常に2つであると考えられることです。
メソッドをデリゲートとして考えると、メソッドのシグネチャはそのまま型と解釈できます。
そもそも今説明で使っている Func<T, TResult> は、標準ライブラリに定義されている
汎用デリゲートですね。
ジェネリック型のメソッドを考えるとき、型は何でもいいけど引数が可変では困ります。
今回書いたような泥臭くめんどうな定義をたくさんしなくてはなりません。
しかし、メソッドがカリー化されている前提で考えると、メソッドの引数が2つだけの
場合を考えるだけでよくなります。
こちらもAddを使って実際に行ってみます。
int Add(int a, int b, int c) // 最初は引数三つ { return a + b + c; } // bとcを受け取るタイミングをずらしたい // イメージ int Add(int a) // 引数は一つで { return ((a + b?) + c!!); } // bを後から受け取りさらにその後にcを受け取りたい // C#で正しく書くと Func<int, Func<int, Func<int, int>>> AddCurry(int a) // 返すのはデリゲートを返すデリゲートで引数ひとつ { return x => y => Add(a, x, y); } // クロージャが(ry // 使ってみると Func<int, Func<int, int>> appliedOnce = Add(1); // x => y => Add(1, x, y) Func<int, int> appliedTwice = appliedOnce(2); // 2 => y => Add(1, 2, y) int result = appliedTwice(3); // 2 => 3 => Add(1, 2, 3)
どの段階でもちゃんと引数が一つになりました!
ラムダ式をネスト?させる部分が綺麗で実装しやすいです。
こんな感じのがカリー化です。
Delayメソッド
いまふとLazyの方がいいかなぁと思いましたがとりあえずDelayメソッドです!
その名の通り遅らせてくれます。
何が遅れるかは、メソッドへ引数を適用して処理させるのを遅らせてくれます。
やっていることは部分適用がわかれば一瞬でわかって、引数が0になるように
部分適用しているだけです。
Addで見てみましょう。
int Add(int a, int b, int c) // 最初は引数三つ { return a + b + c; } // この計算を、引数が与えられた瞬間でなく好きな時にさせたい // イメージ int Add() // 引数はなしで { return (a + c + d); } // aとbとcは前もって渡したい // C#で正しく書くと Func<int> AddDelay(int a, int b, int c) // 返すのは引数なしのデリゲート { return () => a + b + c; } // クロージャが(ry // 使ってみると Func<int> addDelay = AddDelay(1, 2, 3); // () => 1 + 2 + 3 でまだ実行されない int result = addDelay(); // この時点で初めて変数の中身を評価
ちょっとわかりづらいかもですが、引数を渡した段階では中身をのぞいていません。
引数がオブジェクト型でnullを渡したとき、どこでぬるりとなるか想像するとわかりやすいです。
Match
タプルのパターンマッチングをしてくれるメソッドです。
標準で定義されていないからタプルが普及しないのではと思うレベルのものです。
C#7.0には期待しているのでお願いします。。。
やってることは単に値を参照渡ししてタプルの中身を書きだしてくれるだけですね。
使うとこんな感じ。
int n; string s; List<int> l; // 変数宣言 var tuple = Tuple.Create( // タプルの生成 1, "に", new List<int>(new int[]{3}) // Tuple<int, string, List<int>> ); tuple.Match(out n, out s, out l); // さくっと移す Console.WriteLine(n + s + l[0]); // => 1に3
わざわざItem1とかItem2とかでアクセスしなくてよくて楽ちん。
おわりに
ジェネリック型のメソッドは、とにかく定義が非常に面倒で、何に使っていいかも
わかりづらいものだと思います。
それは、オブジェクト指向しているとメソッドの引数は抽象的になるけども
処理自体は具体的なものになりがちだからかなと。
だから引数の型が不定なのにどんな処理を記述すればいいの?ってなるのだと思います。
ですので、一度具体的な実装から離れて構文の中に自分の世界を作ってやる!くらいの気持ちで
フレームワークみたいなのを作ってみるとジェネリックの良さが見えてくるのではと思いました。
個人的には、ジェネリックはコンテナに使うかデリゲートを渡してなんぼだと思います。
ネタでぐちぐち書くだけのつもりでしたが綺麗にまとまったきがします!
それではまたよろしくお願いします。
参考
yohshiy様(2014) C# でのカリー化の使用 | プログラマーズ雑記帳
先日のエントリでも参考に2回使わせていただいたyohshiyさんのエントリです。
僕の大雑把で直観任せの説明よりも丁寧でわかりやすいのでぜひ読んでみてください。
lironi5様(2013) C# のクロージャと部分適用とカリー化 - チラシの裏でプログラミング
さらっと知っている前提のように書いてきたクロージャについて解説されています。
このクロージャで嬉しいことの説明に部分適用とカリー化が引き合いに出されています。
基本的にクロージャさえ押さえておけば部分適用もカリー化も抑えたような物なので必見です。