C#に型クラスを入れる実装の話

この記事はC# Advent Calendar 2016の12日目のものです。 昨日(今日)書いた、F# Advent Calendar 2016 11目C#版です。

今日のリポジトリはこちら。

github.com

実は、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 にしたい」という場合を考えてみましょう。

public static IEnumerable<T1> FirstAll<T1, T2>(IEnumerable<KeyValuePair<T1, T2>> xs)
    => xs.Select(kvp => kvp.Key);
public static IEnumerable<T1> FirstAll<T1, T2>(IEnumerable<Tuple<T1, T2>> xs)
    => xs.Select(t => t.Item1);
public static 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でなければならないことを制約で書く
public static IEnumerable<T1> FirstAll<T1, T2, implicit Tpl2>(IEnumerable<Tpl2> xs) where Tpl2 : Tuple2<T1, T2>
    => xs.Select(x => First(x)); // 本体部分では何の修飾もなしにメソッドを呼び出す

// 当然、SecondAllも同様に定義可能
public static IEnumerable<T2> SecondAll<T1, T2, implicit Tpl2>(IEnumerable<Tpl2> xs) where Tpl2 : Tuple2<T1, T2>
    => xs.Select(x => Second(x));

これで、定義した FirstAllSecondAll には IEnumerable<KeyValuePair<TKey, TValue>>IEnumerable<Tuple<T1, T2>>IEnumerable<(T1, T2)> も渡せます。 このように、既存の型に対して後付けで新たな抽象を追加できるのがコンセプトの便利なところの一つです。

ここからは未確認ですが、おそらく戻り値オーバーロードのようなこともできるようです。

public static 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.SequentialEqualIEnumerable<T> どうしの比較をしていますが、比較不可能なもの((Equals をオーバーライドしていないとかとか))でもコンパイルが通ってしまいますが、コンセプトが導入されれば Eq コンセプトの要素を持つ場合のみに有効な比較演算子みたいなものも定義出来てうれしい、とかがあったりします。

この実装方法の利点・欠点

この実装方法は、既存のランタイムに全く手を加える必要がないのが利点です。

欠点は、この実装方法でどこまでやるかという話になりますが、例えば == 演算子Eq コンセプトで置き換えるとなると、互換性を犠牲にする必要が出てきてしまう点です。 全部作り直してしまえるタイミングはとうの昔に過ぎ去っているので、別の演算子を導入するとか何らかのスイッチで切り替えられるようにしておくとかしないといけません(そんなの知るか、全部作り直しじゃー!ってのも面白いんですけどまずないでしょう)。

C#にコンセプトはいつ乗るの?

この実装が乗ることはまずないです。 ですが、こういう「今ここにない機能」が実際に動作するコードとともに公開されているというのは、いい時代になったものです。 コンセプト(≒型クラス)は、Haskellはもちろん似たような機能がSwiftやRust、Scalaといった今をときめく言語たちに乗っていますので、この実装そのままではなくても、いつかはC#にも乗ったりする日が来るかもしれませんね。

re:僕にとってMaybe / Nullable / Optional が、どうしてもしっくりこないわけ。

元ネタ: 僕にとってMaybe / Nullable / Optional が、どうしてもしっくりこないわけ。 - 亀岡的プログラマ日記

OOPの文脈で見ると、元の記事が言っていることもわからなくはないのですが、対象が広すぎていろいろと不正確になってしまっているので、ちょっとまとめてみます。

元の記事が対象にしているのは、Maybe / Optional / Nullableの3つです。 対応する型を持つ言語を見てみると、下記のようになります。

これらは、「値がないこと」を表すもの、という見方では同じですが、それぞれ異なる価値観の元に作られています。

Maybe/OptionalとNullable

これらはすべて型パラメータを取ります*1。 しかし、この中でNullableだけは型パラメータに値型のみという制約が付きます。 これは、C#Nullableは他のものとかなり性質が違うことを意味しています。 元の記事では並べて書かれていましたが、そもそもNullableに関してはここに並べるべきものではありません。

C#のNullable

C#Nullableは、値型にもnullが使いたいという動機で導入された機能です。 そのため、NullableMaybeOptionalと違って、むしろnullを積極的に使えるようにするための機能と言えるでしょう。 これによってC#の上ではNullableを使うことでコード上の見た目だけに関しては、値型でもnullが使えるように見せています。 見た目だけの話であれば、SwiftOptionalと同じように型名の後ろに?が付くため、同じような言語機能に見えますが、実際には全くの別物と考えたほうがいいでしょう。

JavaのOptional

JavaOptionalは、メソッドの戻り値として使うことが想定されているらしく、引数やフィールドに使うことは想定されていないそうです。 JavaOptionalは参照型なので、Optional自体がnullになりうるという点を考えると、引数に使うべきではないというのもある程度納得できます*2

Javaでは現状、型パラメータに指定できるのは参照型だけであり、nullになりうるのも参照型だけです。 そのため、JavaOptionalC#Nullableと違って、(戻り値としての)nullを排除しよう(少なくとも、減らそう)という動機で導入されたとみていいでしょう。

SwiftのOptional

SwiftOptionalは、Javaと名前は同じですが、より言語の仕組みに根差しています。 Javaでは、Optional<T>型の変数にT型の値を代入できません。

Integer i = 42;
Optional<Integer> x = i; // コンパイルエラー

それに対して、Swiftではそれが可能です。

var i: Int = 42
var x: Optional<Int> = i // OK
// Optional<Int>はInt?とも書ける

SwiftではOptionalではない型にnull*3は代入できません。 ですが、Optionalという仕組みによって今までの知識からあまり外れない書き方でnullを扱えるようになっています。

HaskellのMaybe

HaskellMaybeは実装としてはSwiftOptionalに一番近くなっています。 ただ、他の言語と違ってHaskellではそもそも他の言語のようなnullはなく、Swiftのように既存言語ユーザーを取り込むために「今までの知識からあまり外れない書き方」というのも求めていません。 結果だけ見ると、JavaOptionalと同じように、Maybe t型の変数にt型の値は代入できません。

Nullのダメな理由

元記事では、「Nullは何がダメなんだっけ?」について、

  1. Nullは型ではない
  2. あるメソッドがNullになるかどうかの判断ができない

が挙げられています。 細かい点ですが、ここにも誤解があります。

(少なくともJavaでは)Nullは型を持つ

まず、Javaの話であればnullは型を持っています*4。 そのため、「ClassHogenull」という表現は、少なくともJavaにおいては正確ではなく、正確には「ClassHoge型に暗黙変換されたnull」とでもなるでしょうか。 詳しくは言語仕様を参照してください。

ここで言いたかったのはおそらく、「nullはどんな型にも変換できてしまう」ということでしょう。

あるメソッドの結果(もしくは変数)がNullかどうかの判断が型だけからできない

これも細かいですが、あるメソッド(の結果)がNullになるかどうかの判断ができないというのは、正確には「型を見ただけでは」できないのであり、元記事中にあるように、if文によって実行時にはチェックできます。

本題

さて、ここからが本題です。 元記事では、Optionalが解決するのは2だけ、としていますが・・・

Javaの場合

JavaOptionalでは、Optional<T>型とT型は全くの別物であり、相互に暗黙変換できません。 そのため、

  1. (nullに相当する)Optional.empty()Optional型の変数にしか入れることはできない*5
  2. あるメソッドの結果がOptional.empty()になりうるかどうか型だけで判断できる

となります。

Swiftの場合

SwiftOptionalは、Optional<T>型の変数にT型の値を代入できますが、その逆はできません。 そのため、

  1. (nullに相当する)nilOptional型の変数にしか入れることはできない
  2. あるメソッドの結果がnilになりうるかどうか型だけで判断できる

となります。

Haskellの場合

HaskellMaybeは、Maybe t型とt型は全くの別物であり、相互に暗黙変換できません。 そのため、

  1. (nullに相当する)NothingMaybe型の変数にしか入れることはできない
  2. ある関数の結果がNothinsになりうるかどうか型だけで判断できる

となります。

元記事が言いたかったこと

このように、OptionalもしくはMaybeは、1も2も解決できています。 これは十分なメリットであり、このメリットだけでもOptional/Maybeは有用です。

ただ、元記事を見ると、

本当に解決したいのは「Nullに型独自の処理をさせたい」なので、残ったままです

とあります。 つまり、本当に問題にしていたのは、「型Aの値がない場合と型Bの値がない場合で別の処理をさせたい」となります。

if文相当の処理を書く必要性

Optional/Maybeを使ったコードで頻繁にif相当の処理を書いてしまっている場合、それはそもそもOptional/Maybeの使い方を間違っている可能性が大きいです。 リストを操作する関数にリストを返す関数が多く含まれるのと同じように、あるいはTaskを返す関数を内部で呼び出している関数がTaskを返す関数になるのと同じように、Optional/Maybeを返す関数を内部で呼び出している関数は、その関数の戻り値もOptionalになることがよくあります。 この場合、if相当の処理を書くのは間違っています。

// Javaだと思う(Swiftは書いたことないので)
Optional<User> user = findUser(pred);
if (user.isPresent()) {
    // 何かする
    return Optional.of(何か処理の結果);
} else {
    return Optional.empty();
}

例えば、「何かする」の部分がuserの中身を使った関数を呼び出すような処理の場合、

// 値がなかった場合のことは気にしない
return findUser(pred).map(user -> 何かする関数(user)); // ラムダ式を使わずに直接関数を渡す、でも可

のようになるでしょう。 mapの他にも、様々なメソッドが用意されているため、ifによる分岐に頼る場面は少なくなります。 Haskellの場合、do構文によってさらに複雑な例でもすっきり書けますが、それはまた別の話。

手続き的には見えない

さて、上のコードが手続き的に見えるでしょうか? 呼び出し側はOptionalのチェックを行っておらず、コードも短く簡潔です。

ではオブジェクト指向プログラミングではどのようになるでしょうか? Userに対してNullObjectを用意して、このようになるでしょう。

public interface User {
    戻り値の型 何かする処理();
}
public class UserImpl implements User {
    @Override
    public 戻り値の型 何かする処理() {
        // 何かする
        return 何かした結果;
    }
    // ...
}
public class EmptyUser implements User {
    @Override
    public 戻り値の型 何かする処理() {
        return 戻り値の型のNullObject;
    }
    // ...
}
// 呼び出し側は意識しない(意識できない)
return findUser(pred).何かする処理();

呼び出し側だけ見ると、Optionalのときとそんなに変わってませんよね*6。 つまり、元のコードも手続き的には見えないと言っていいでしょう。

NullObjectをちゃんと使いたい、というよりは、Optionalをどう使うか考えたい

OOPの文脈で、どこまでOptionalを使えばいいのかというのはよくわかりません。 Optionalを使うと、どうしてもコードはOOPから離れて行ってしまうという感覚は確かにあります。 どのあたりでバランスを取るのがいいのかはケースバイケースでしょうし、一般化できるものではないと思っています。

コメント欄では id:kyon_mm が(やはりOOPの文脈で)

Optionを返していいのはむしろprivateやprotected的なものだけだと思っていて、それ以外はオブジェクトだったり、バリアント(判別共用体)的な感じで返すのが「オブジェクトとやり取りをしていて、それぞれに責務が割あたっている」といえるのではないだろうか。と思っています。

と言っています。 これはこれで一つの態度ではありますが、基本的なライブラリでは Optional を返す関数*7というのはもっと積極的に作られることになると思います。

FPに軸足を置くか、OOPに軸足を置くかは、対象の性質やメンバーのスキル等によって決めていくしかないでしょう。 ただし、間違ったOptionalの使い方をもって「しっくりこない」というのであれば、まずはOptionalの正しい使い方を学び、実践すべきです。 そのための言語としては、F#なんていかがでしょうか。

*1:Javaは取らなくても許されるとかいう細かい話は置いといて。

*2:フィールドに使うべきではない理由はよくわかりませんが

*3:Swiftではnilだが、この記事ではnullで統一

*4:ただし、その型の変数やフィールドを作ることはできない

*5:Objectは無視します

*6:findUserがnullを返す可能性は考慮しなくていいのかという点はここでは考慮しない

*7:もしくは、Optionalを返す関数を受け取る関数

ChalkTalk CLR – 動的コード生成技術(式木・IL等)に行ってきた

centerclr.doorkeeper.jp

直前で定員を増やしてもらえたので、参加できました。

会自体について

内容は主にILの話と式木の話で、ディスカッションというよりは講義に近い感じでした。 個人的にはディスカッション寄りの会を期待していたのですが、知識レベルにばらつきがあったのと、初めての取り組みということで仕方ないのかな、と。 次回以降で、もしディスカッション寄りの会をやりたいなら、「この本を読了しており内容についてもある程度理解していること」とかにした方がより濃い内容のディスカッションができると思います。 聴講のみの席も用意すると、「ディスカッションに参加するのは怖いけど話は聞きたい」って人も参加できますし*1

最初にKPTをして今回の方向性を決めよう、という試みは面白くはありましたが、会の最初にKも何もないので、KPTにとらわれずに「知りたいこと」「議論したいこと」みたいな分類から始めればいいんじゃないかなぁ、と思いました。 ポジションペーパーを用意してもらって、軽く自己紹介でもいい気はします。

会の内容について

IL

ILについては、ILの存在意義的な話から始まり、C#のコードがどのようなILに落ちるのかをIL Spyを使って見たり、出力されたILがどのように実行されるのかを図にして追ったりする内容でした。 「DebugでコンパイルしてもReleaseでコンパイルしても吐かれるILにさほど差は出ない」と説明があったときには、「C#で構造が大きく変わることは見たことないけど、(nopは置いといても)発行されるILはそれなりに変わるよ!」とか、「F#の場合は構造も結構がらりと変わるよ!」って突っ込みを入れようかと思いましたが自重しました。 最初のKPTでそういう会じゃないというのはわかったので、それを考えると最初のKPTはある程度効果があったと思います。

それと、最初は OpCodes を参考にすればいいでしょうが、ある程度ちゃんとやるなら Standard ECMA-335 は手元に置いておきましょう。

ILはどうやって学べばいいのか?という話の流れで、ILをどうやってデバッグすればいいのか、という話になったので、「生成するIL(が生成するdll)にデバッグ情報埋め込めるから、それすればデバッグできるよ」という話と、「IL Support extension使えばC#(やVBやF#)とILをいい感じに統合できるしデバッグもできるよ」という話をしました。 が、後者のデモ(実際にILをデバッグする例)のせいで前者がちゃんと伝わっていない気がします。こっちもデモすればよかったんですが、手持ちがF#のコード例のみだったので自重したのがいけなかったか・・・ 詳細については、メタプログラミング.NET を熟読しましょう。5.4.2です。Kindle版は安いのでおすすめです。

式木

式木については、木構造の話からはじめ、C#の式木は今や式だけじゃなくて文も扱うよ、という話などをしました。 思うに、式木の使い方っていくつかに分類されて、それぞれで目的がバラバラだからわかりにくいんじゃないでしょうか?

  • 式木を別の構造に移しかえる(最終的にはCompileしてデリゲートに落として.NET上で実行)
  • 式木を別の構造に移しかえた上で何らかの直列化をする(最終的にはSQLなどになって何らかのミドルウェア上で実行など)
  • 式木を走査して情報を取り出す(最終的には.NET上のオブジェクト)

LINQ to EntitiesなどでRDBを使う例は上記の2番目に、メタ情報を取得するために式木を使うことでコードの変更に強くなるよ、という例は上記の3番目に当たるわけです。 ここら辺をひとまとめにして「式木の使い道」で説明しちゃうと分かりにくいのかな、と思ったり。 このあたりはちょっとした図を作るといいのかなぁ・・・

Excel方眼紙について

最初に行ったKPTに、「Excel方眼紙を駆逐する」というものがありました。 それを受けて、メッセージを実行時にExcelから取ってくる仕組みを式木を使って実装した、という話があったんですが、ここは声を大にして言いたい。

それ、Excelから脱却してないじゃん!むしろExcelの活用の話じゃん!!!

はい。 まぁ、そもそも「Excel方眼紙の駆逐」が何を指しているのかあいまいだ、という問題があります。

  • Excel方眼紙なんて不要だから開発現場から全撤廃するべきだ
  • Excel方眼紙を納品しなければならないのは仕方ないけど、プログラマに使わせるのは非効率だからすべてのExcel方眼紙は別の何かから生成されるべきだ
  • Excel方眼紙を納品しなければならないのは仕方ないけど、プログラマに使わせるのは非効率だからプログラマExcel方眼紙に触れなくてもいい環境を作るべきだ
  • Excel方眼紙を使うのは仕方ないけど、それとプログラムの対応付けを手でやるのは非効率だから自動化すべきだ

ざっと思いついただけでもこんな感じで、上に行くほど駆逐の意味合いが強く、下に行くほど弱いです。 このなかで、メッセージを実行時にExcelから取ってくる仕組みは一番下であり、これより弱いものはちょっと思い浮かびません。 と言うわけで、Excel方眼紙を駆逐する、というテーマについてであれば、もっと強いものがほしいと思うのでした。

個人的な取り組みとしては、1ソース(外部DSL)から複数の成果物を出力する方法でプログラマExcel方眼紙を触る必要性をある程度軽減できないかな、ということでTableDslというものを作っています(色々あって停滞中)。 社内ツールになりますが、メッセージ一覧も同様の考え方でプログラマExcel方眼紙を触らなくていいようにしているもの(こっちは内部DSL)も作ってます。 汎用的な仕組みとしては、F# TypeProviderを使ってExcel方眼紙を扱うライブラリを作っている(いた?)のですが、これは今風に書き直したいところです。 これらは2番目の方向性ですね。

3番目の方向性としては、これまた社内ツールとして結合テストの実行を支援するツールがあります。 これは4番目の方向性と似ていますが、「対応付け」でどちらかがどちらかを強く意識する必要がなくなる点で異なります。対応付けを2段階にし、途中にツールがくることでより明確に役割が分離されます。

2, 3, 4番目の方向性で使うのは、ExcelファイルのIOができるライブラリになります。

  • NPOI
  • EPPlus
  • ClosedXml

などが候補になります。COMオートメーション?知らない子ですね。

Excel VBA

ある意味Excel方眼紙よりも面倒なのが、Excel VBAです。 1セル1文字とか無茶なことをしていない場合、Excel方眼紙をプログラム上で扱うのはそれほど面倒ではないですが、Excel VBAを駆使して作られたシステムはヤバいです。 こいつらを置き換えるためには、例えば下記のようなものが使えます。

FCellのデモ動画は強烈なので見てみるといいでしょう。

いいですか、駆逐すべきはExcel方眼紙よりもExcel VBAです。

まとめ

  • IL Support布教できてよかった
  • 動的生成されたILのデバッグのデモすればよかった
  • Excel方眼紙よりもExcel VBAを駆逐したい

*1:一応補足しておきますが、そういう形式で開催しろ、と言っているわけではない

*2:いつの間にかCodePlexからGithubに移動してた・・・

シャドーイングとイミュータブルプログラミング

シャドーイングのない言語と、イミュータブル中心のプログラミング(以下イミュータブルプログラミング)の相性って悪いのでは?と思ったのでブログに残しておきます。

シャドーイングとは

既存の変数と同名の変数を定義して、そのスコープで既存の変数にアクセスできなくする機能です。 例えば、F#ではシャドーイングができるので、

let f x =
  if x % 2 = 0 then
    (* 引数のxをシャドーイング *)
    let x = -1
    printf "%d, " x
  (* スコープが抜けたので、引数のxを表示 *)
  printfn "%d" x

f 10    (* => -1, 10 *)
f 11    (* => 11 *)

となります。

シャドーイングのない言語、例えばC#では同じことはできないので、別の名前を付けるか、再代入で回避することになります*1

public void F(int x)
{
    if (x % 2 == 0)
    {
        var otherX = -1;
        Console.Write("{0}, ", otherX);
    }
    Console.WriteLine(x);
}

シャドーイングの使い道

シャドーイングの使い道としては、例えば以下のようなものがあります。

  • ミュータブルな変数の範囲の制限
  • イミュータブルプログラミングでの状態変数の受け渡し

ミュータブルな変数の範囲の制限

F#にはミュータブルな変数があります。 ですが、一般的なF#プログラマは極力ミュータブルな変数を使いません。 どうしてもミュータブルな変数が使いたくなったとしても、ある時点以降では再代入が行われないと分かっているなら、ミュータブルな変数をシャドーイングすることで、以降で誤って再代入できないことをコンパイラに保証させることができます。

let mutable x = 10
(* xに再代入する場合があるコード *)
...

(* ここからはxに再代入しない *)
let x = x
...

イミュータブルプログラミングでの状態変数の受け渡し

イミュータブルプログラミングしていると、ある変数をクローンして一部分を書き換えた値を作り出すというコードが結構出てきます。 このような場合にシャドーイングを使えば、更新前のいらなくなった変数にアクセスできなくなるため安心してコーディングできます。

let f newKey cache =
  let cache = cache.Clone(key = newKey)
  g cache (* このcacheはシャドーイングされた方のキャッシュ *)

しかし、シャドーイングのない言語ではこれはできません。 簡単に取れる回避策としては、イミュータブルプログラミングを一部捨てて、引数に再代入するか、新しい名前を付けるかです。

public SomethingResultType F(Cache cache, Key newKey)
{
    var newCache = cache.Clone(key: newKey);
    return G(newCache);
}

これは新しい名前を付けた場合です。 しかしこの場合、

public SomethingResultType F(Cache cache, Key newKey)
{
    var newCache = cache.Clone(key: newKey);
    return G(cache);
}

のように間違えて元の変数を使ってしまえます。 というか、仕事で実際に使ってしまいました。 シャドーイングさえあればこんなミスはしませんでした*2し、同じものを表すのに別の名前を付けなければならないのはそもそも違和感があります。

もう一方の「再代入」で回避する方法は、一部とはいえイミュータブルプログラミングを捨てることになるので、他の回避策がある場合に取りたくはありません。 あまりイミュータブルとミュータブルを行ったり来たりしたくないですしね。

ということで、シャドーイングのない言語はイミュータブルプログラミングと相性が悪いのではないでしょうか?*3 シャドーイングがない言語では、イミュータブルプログラミングを全面的に採用するのはあきらめたほうがいいな、というのが現時点での考えです。

*1:ただし、そうそうないが、既存の変数と新しい変数の型が違う場合は再代入では対応できない場合もある

*2:typoしてシャドーイングしたつもりができていなかった、ということもあり得ますが・・・

*3:Stateモナド等を使って状態変数を隠すというのも考えられなくはないですが、シグネチャ壊しちゃうのでいろいろ面倒

なごやかJavaで発表してきた

なごやかJava第一回で、「.NET系開発者から見たJava」というタイトルで発表してきました。 Javaのこの機能って.NET開発者から見てどうなの?というような内容です。

大阪から参加してくれた方の感想を載せておきます。

JavaでTupleってみる

可変長な型変数の表現より、タプルの話です。

.NETのタプル

.NETでは、型引数の数が違うクラスを定義できるので、Tupleという名前で7要素まで対応しています。

public class Tuple<T1> { ... }
public class Tuple<T1, T2> { ... }
public class Tuple<T1, T2, T3> { ... }
public class Tuple<T1, T2, T3, T4> { ... }
public class Tuple<T1, T2, T3, T4, T5> { ... }
public class Tuple<T1, T2, T3, T4, T5, T6> { ... }
public class Tuple<T1, T2, T3, T4, T5, T6, T7> { ... }

そして、各要素にアクセスするために、Item1, Item2のようなプロパティを使います。

さて、では.NETのタプルは8要素以上は対応していないかと言うと、そうではありません。 タプルのインスタンスを作るためには、Tuple.Createというメソッドを使って作るのが楽です。 そしてこのTuple.Createは、なんと8引数版まで用意されているのです。

var t = Tuple.Create(true, 2, 3.0, "4", new[] { 5 }, 6M, 7L, 8);

このtの型は、こうなります。

Tuple<bool, int, double, string, int[], decimal, long, Tuple<int>>

実は、Tupleは8番目の型引数として、「残りの要素を保持するタプルの型」が受け取れるようになっています。 ちなみにTuple<T1>という謎のクラスはこの時のみ使われるクラスです。

public class Tuple<T1, T2, T3, T4, T5, T6, T7, TRest> { ... }

「残りの要素を保持するタプル」にアクセスするためには、Item8ではなく、Restという名前のプロパティを使います。 そのため、8要素タプルの8番目の要素にアクセスしたい場合は、

var value = t.Rest.Item1;

となります。 さてさて、ここからが.NETの残念なところなのですが、実は9要素以上のタプルを作る簡単な方法は用意されていません*1。 ので、コンストラクタを使うことになります。

var t = new Tuple<int, int, int, int, int, int, int, Tuple<int, int>>(
    1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9));

コンストラクタの型パラメータは省略できないので、悲惨なことになっていますね・・・ これは、Tuple.Createの8要素版が8要素タプルを作るためではなく、ネストしたタプルを作るためのヘルパーになっていればもっと楽が出来たのです。

var t = Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9)); // こうはなっていない

残念すぎる・・・

Restという考え方

さて、.NETのタプルは残念ですが、「足りない部分はRestで」というのはどこかで聞いたことのあるような話です。 そう、コンスセルですね!

ということで、コンスセルの考え方を使ってタプルのないJavaにタプルを作ってみましょう。 まずは、コンスセルの終端を表すためのクラスを導入します。 以降では、特にパッケージとか書きませんけど、全部同一パッケージに入っていると思ってください。

public final class TupleNil {
    static final TupleNil nil = new TupleNil();
    private TupleNil() {}
}

簡単ですね。 こいつは状態を持っておらず、インスタンスも外部からは生成できず、 さらには唯一の静的フィールドであるnilもパッケージプライベートなので、 パッケージ外からはこのフィールドにすらアクセスできません。 これだけでは本当に全く何の役にも立たないクラスです。

次に、2つの型パラメータを取って実際に要素を保持するクラスを作ります。

public final class TupleCons<T, Rest> {
    public final T value;
    public final Rest rest;

    TupleCons(T value, Rest rest) {
        this.value = value;
        this.rest = rest;
    }
}

これも簡単ですね。 単に、値を2つ持っているだけのクラスです。 このクラスのvalueに値が、restに残りの要素を表すものが格納されます。

コンストラクタがパッケージプライベートなので、パッケージ外からこのクラスのインスタンスを生成することはできません。

最後にタプルを作るためのメソッドを持ったクラスです。

public final class Tuple {
    private Tuple() {}

    public static <T> TupleCons<T, TupleNil> singleton(T value) {
        // こことか
        return new TupleCons<T, TupleNil>(value, TupleNil.nil);
    }

    public static <T1, T2, Rest> TupleCons<T1, TupleCons<T2, Rest>> cons(T1 value, TupleCons<T2, TupleNil> rest) {
        // ここってダイアモンド演算子使えるんですか?使えそうだけどJavaとかわかりません><
        return new TupleCons<T1, TupleCons<T2, Rest>>(value, rest);
    }
}

あとは、これを使ったコードです。

TupleCons<Integer, TupleCons<String, TupleNil>> t2 = Tuple.cons(42, Tuple.singleton("hoge"));
System.out.println(t2.value); // 42
System.out.println(t2.rest.value); // hoge

やったー!(何が

まとめ(ない)

  • .NETのタプルは割と現実見て、7要素までは自然に扱える
    • 大量要素のタプル使うな
    • でももし扱う場合があるといけないから、一応対応しとくぜ!
  • .NETのTuple.Createはクソ
    • 8要素版をなぜそういう形で用意したし
    • 9要素以上のタプルを作りたい場合はコンストラクタで頑張るしかない
    • それLangExtならCreate.Tuple(1, 2, 3, 4, 5, 6, 7, 8, 9)でできるよ!
    • それF#なら(1, 2, 3, 4, 5, 6, 7, 8, 9)でできるよ!
  • タプルってコンパイル時のリスト(コンスセル)だよね!
    • Javaでそういう実装してみた
    • 元記事が後ろに後ろに拡張していくのに対して、こっちは(consで)前に前に拡張していくという違いがちょっと面白い
    • 元記事がインスタンスメソッドで拡張していくのに対して、こっちはクラスメソッドで拡張していくのもちょっと面白い
      • インスタンスメソッドにもできるけど、キモイことになる(字面上は後ろに書いたものが前に追加される)のでやめた
    • 実用性?しりませんなぁ

*1:まぁ、そんなタプル作るなと言われればその通り、としか言えないですが

オーバーロードって素晴らしいですよね!

オーバーロード

いやぁ、オーバーロードって素晴らしいものですよね。 例えばC#でintを取るメソッドと、stringを取る同じ名前のメソッドを書きたくなったとするじゃないですか。 そんな時でも、C#はメソッドのオーバーロードが出来るので、こう書けるわけですよ。

public Hoge Something(int x) { ... }
public Hoge Something(string str) { ... }

素敵ですね!

関数(Funcデリゲート)

では、関数を考えてみましょう。 非ジェネリックなメソッドはreadonlyなフィールドとしても定義できますよね。

public static Func<int, Hoge> Something = ...

このSomethingは、他のメソッドと同じように呼び出せます。 SomethingがHogeクラスに定義されていたとしたら、

var res = Hoge.Something(10);

と出来るわけです。 メソッドの時と変わりませんから、簡単ですね。

ではこれに、stringを受け取ってHogeを返す関数も追加して・・・

public static Func<int, Hoge> Something = ...
public static Func<string, Hoge> Something = ...

出来ない!!!

はい。 フィールドやプロパティはオーバーロード出来ないのですね・・・

ということで、関数をフィールド(やプロパティ)として定義出来るように見えたとしても、 C#ではフィールドやプロパティのオーバーロードが出来ないため、メソッドでできること全部は実現できません*1

関数を返すメソッド

ここで、関数を返すメソッドを考えてみましょう。 例えば、intを受け取ると「stringを受け取ってHogeを返す関数」を返すようなメソッドです。

public static Func<string, Hoge> Something(int i) { ... }

この関数は、引数を1つ渡すと関数が返ってくるので、そこにさらに引数を渡すことでHoge型の値が返ってきます。

// Hoge.Something(10)で返ってきた関数に"hoge"を渡す
var res = Hoge.Something(10)("hoge");

これ、2つ引数を取るメソッドと似てませんか?

// public static Hoge Something(int i, string str) { ... } があったとして、
var res = Hoge.Something(10, "hoge");

どちらも、引数を2つ渡すことでHogeが得られます*2

再びオーバーロード

で、ですね。 引数を2つ取るメソッドはオーバーロード出来ます。

public static Hoge Something(int i, string str) { ... }
public static Hoge Something(int i, int j) { ... }

でも、同じようなことが出来る関数を返すメソッドはオーバーロード出来ません。

public static Func<string, Hoge> Something(int i) { ... }
public static Func<int, Hoge> Something(int i) { ... }

C#では、戻り値の型が異なるだけのメソッドがオーバーロード出来ないのです。 ここでも、戻り値の型のオーバーロードが出来ないため、メソッドで出来ること全部を実現することができません。

オーバーロード、確かに便利だけど、関数ではできないのがちょっと残念ですね。 最近だと関数を使う場面というのは多くなっているからなおのこと残念さが増します。

F#はどうか

F#では、関数ではオーバーロードを許していません。

何!?いまどきオーバーロードないだって!?!?そんな言語使えるかー!!!

判別共用体という解決策

メソッドではオーバーロードが使えるんですが、ここでは判別共用体を使いましょう。 判別共用体は、F#でユーザ定義型を提供する方法の一つで、「この中のどれか一つ」を表すことのできる型を定義できます。 これを使うと、something関数に渡せる型を次のように定義できます。

(* 文字列か整数を表す型 *)
type SomethingArgType =
  | Str of string
  | Int of int

非常に簡単に型が作れることが分かります。 この型を使うと、somethingの実装はこう書けます。

let something = function
| Str str -> ...
| Int i -> ...

呼び出し側はこうです。

let res = something (Str "hoge")

オーバーロードするために型を作るの、ちょっとだるい気もしますけど、簡単に型が作れるので割とありな気はします。 他にも、別モジュールに格納するとか、そもそも関数名を分けるという方法も考えられ、まぁ一番適切だと思うものを選択すればいいです。

さて、F#にはオーバーロードがありませんので、関数を返す関数だろうが関係ありません。

(* 上のSomethingArgTypeとかsomethingとは別物 *)
type SomethingArgType =
  | Str of string
  | Int of int
(* 引数を2つ受け取るsomethingを定義 *)
let something i = function
| Str str -> ...
| Int j -> ...

(* 使う *)
let res = something 10 (Str "hoge")
let res = something 10 (Int 20)

あっ・・・オーバーロードいらない・・・

他にも、オーバーロードを削ったおかげで関数の型推論が出来るだとかのメリットもありますが、それはまた別の時にでも。

何が言いたいか

オーバーロードがいらないは完全に言い過ぎですけど、オーバーロードを入れてしまったがために(後から導入した)関数との統一性がなくなってしまっています。 「便利そうだから」という理由だけで言語に機能を盛り込むのではなく、 導入することによるデメリット(将来予定している機能との相性はどうか、とか)も考えたうえで盛り込んでほしいものですよね。 C#が登場した時期に関数型言語の機能を将来取り入れることを見越していたかは正直微妙*3ですが・・・

*1:他にも、ジェネリックメソッドを関数で置き換えることもできません。

*2:関数を返すバージョンは、引数を一度に与える必要がないという違いはあるけど、ここではそこには触れません。

*3:delegateはあるけど、ううむ