この広告は、90日以上更新していないブログに表示しています。
この記事はC# Advent Calendar 2016の12日目のものです。昨日(今日)書いた、F# Advent Calendar 2016 11目のC#版です。
今日のリポジトリはこちら。
実は、F#版だけじゃなくてC#版の実装もあります。ということで、そのざっくりした紹介です。
F#版では「型クラス(type class)」と呼んでいますが、C#版では「コンセプト(concept)」と呼んでいるようです。で、コンセプトがあると何がうれしいかですが、例えばC#には現在3つの2要素タプルがあります。
System.Collections.KeyValuePair<TKey, TValue>System.Tuple<T1, T2>(T1, T2)これらの型すべてに対応するためには、現在のC#ではオーバーロードを3つ書く必要があります。例えば、「2要素タプルのIEnumerable から1番目の要素を取り出したIEnumerable にしたい」という場合を考えてみましょう。
publicstatic IEnumerable<T1> FirstAll<T1, T2>(IEnumerable<KeyValuePair<T1, T2>> xs) => xs.Select(kvp => kvp.Key);publicstatic IEnumerable<T1> FirstAll<T1, T2>(IEnumerable<Tuple<T1, T2>> xs) => xs.Select(t => t.Item1);publicstatic IEnumerable<T1> FirstAll<T1, T2>(IEnumerable<(T1, T2)> xs) => xs.Select(t => t.Item1);
面倒ですね。ここで、提案されているコンセプトを使った場合にどうなるか見てみましょう。
// 新しいキーワードconceptを使ってコンセプトを定義public concept Tuple2<Tpl, [AssociatedType] T1, [AssociatedType] T2>{ T1 First(Tpl t); T2 Second(Tpl t); Tpl Make(T1 item1, T2 item2);}// 新しいキーワードinstanceを使ってコンセプトのインスタンスを定義// ここではKeyValuePairをTuple2のインスタンスにしているpublic instance KeyValuePairTuple2<T1, T2> : Tuple2<KeyValuePair<T1, T2>, T1, T2>{ T1 First(KeyValuePair<T1, T2> t) => t.Key; T2 Second(KeyValuePair<T1, T2> t) => t.Value; KeyValuePair<T1, T2> Make(T1 item1, T2 item2) =>new KeyValuePair<T1, T2>(item1, item2);}// System.TupleをTuple2のインスタンスにするpublic instance TupleTuple2<T1, T2> : Tuple2<Tuple<T1, T2>, T1, T2>{ T1 First(Tuple<T1, T2> t) => t.Item1; T2 Second(Tuple<T1, T2> t) => t.Item2; Tuple<T1, T2> Make(T1 item1, T2 item2) => Tuple.Create(item1, item2);}// System.ValueTupleをTuple2のインスタンスにするpublic instance ValueTupleTuple2<T1, T2> : Tuple2<(T1, T2), T1, T2>{ T1 First((T1, T2) t) => t.Item1; T2 Second((T1, T2) t) => t.Item2; (T1, T2) Make(T1 item1, T2 item2) => (item1, item2);}
こういう定義をしておけば、あとは一つだけ実装を書くだけです。
// 型パラメータにimplicitを付けて、その型パラメータがTuple2でなければならないことを制約で書くpublicstatic IEnumerable<T1> FirstAll<T1, T2,implicit Tpl2>(IEnumerable<Tpl2> xs)where Tpl2 : Tuple2<T1, T2> => xs.Select(x => First(x));// 本体部分では何の修飾もなしにメソッドを呼び出す// 当然、SecondAllも同様に定義可能publicstatic IEnumerable<T2> SecondAll<T1, T2,implicit Tpl2>(IEnumerable<Tpl2> xs)where Tpl2 : Tuple2<T1, T2> => xs.Select(x => Second(x));
これで、定義したFirstAll やSecondAll にはIEnumerable<KeyValuePair<TKey, TValue>> もIEnumerable<Tuple<T1, T2>> もIEnumerable<(T1, T2)> も渡せます。このように、既存の型に対して後付けで新たな抽象を追加できるのがコンセプトの便利なところの一つです。
ここからは未確認ですが、おそらく戻り値オーバーロードのようなこともできるようです。
publicstatic IEnumerable<Tpl2> Singleton<T1, T2,implicit Tpl2>(T1 x, T2 y)where Tpl2 : Tuple2<T1, T2> => Enumerable.Repeat(Make(x, y),1);IEnumerable<KeyValuePair<string,int>> res1 = Singleton("aaa",0);IEnumerable<Tuple<int,int>> res2 = Singleton(10,20);IEnumerable<(string,string)> res3 = Singleton("hoge","piyo");
他にも、例えば今はEnumerable.SequentialEqual でIEnumerable<T> どうしの比較をしていますが、比較不可能なもの((Equals をオーバーライドしていないとかとか))でもコンパイルが通ってしまいますが、コンセプトが導入されればEq コンセプトの要素を持つ場合のみに有効な比較演算子みたいなものも定義出来てうれしい、とかがあったりします。
この実装方法は、既存のランタイムに全く手を加える必要がないのが利点です。
欠点は、この実装方法でどこまでやるかという話になりますが、例えば==演算子をEq コンセプトで置き換えるとなると、互換性を犠牲にする必要が出てきてしまう点です。全部作り直してしまえるタイミングはとうの昔に過ぎ去っているので、別の演算子を導入するとか何らかのスイッチで切り替えられるようにしておくとかしないといけません(そんなの知るか、全部作り直しじゃー!ってのも面白いんですけどまずないでしょう)。
この実装が乗ることはまずないです。ですが、こういう「今ここにない機能」が実際に動作するコードとともに公開されているというのは、いい時代になったものです。コンセプト(≒型クラス)は、Haskellはもちろん似たような機能がSwiftやRust、Scalaといった今をときめく言語たちに乗っていますので、この実装そのままではなくても、いつかはC#にも乗ったりする日が来るかもしれませんね。
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。