なんかできたものと、バリアントについて色々

bleisさんのLangExtでのOptionの設計 - ぐるぐる~を読んだ後気づいたら何かよくわからないものが出来上がっていたので、その紹介とかです。

GenericVariant

今回できたものはこちら(GitHub) GenericVariant

Genericじゃない気もするしよくわからない名前ですが、要は無名のバリアントが作れたりするものです。 (名前と実体の相違が甚だしいですが一応ジェネリックス使ってるので許して下さい)

var foo = new[]{
    Variant<int,string>.C1(4),
    Variant<int,string>.C2("foo")};
foo
    .ForEach(either => either
        .Match(
            C1: i => i.ToString(),
            C2: s => s)
        .Act(Console.WriteLine));

みたいな感じで使えます。(ForEachとActは今回のとは関係ないので適当に察してください)

実用的ななにかではないので、へー面白いと思って貰えたらそれだけでいいです。

以下戯言が続くので、どんな感じかもうちょっと見たい人は上のリポジトリのUsageを適当に見ると良いと思います。

バリアントとは何か

例えばF#やLangExtで言うOption[T]型はだいたい

Option[T] = None:Opiton[T] | Some:T->Option[T]

のような型ですが、ここに出てくる型名やラベルを除けば、
#がその型そのものを表すとすれば

# | T->#

と言ったかたちになります。
これはHaskellのMaybeのような同じだけど名前の違うものまで含んだ表現です。
ここでNoneにあたるとこが関数じゃないのがちょっと気になるので#をUnit->#と見ると

Unit-># | T->#

となります。

また、List[T]も上記記法で書くと

Unit-># | (T,#)->#

となります。

以上のように、バリアント型は、ある型からバリアントそれ自体への関数、すなわちコンストラクタの集まりによって定義されます。 つまりバリアントのアイデンティティを決めるのは結局コンストラクタがとる型は何かということになります。

GenericVariantにおいては

そこで、例えばOption[T]にあたる型はVariant<Unit,T>として作れるようにしようと思って作ったのがGenericVariantということになります。

しかしこれではラベルも型名も扱いづらいため、UsageではVariant<Unit,T>に委譲する形で名前をつけOption[T]を書いています。 またUsageでやっているようにList[T]のように再帰型(#がコンストラクタの引数の型に含まれる型)だと、 Variant<Unit,Tuple<T,ここどうするの>>となるので名前を付けないと書けません。

バリアントとレコード

バリアントと対をなすものとしてレコードがあります。 レコードはバリアントのコンストラクタと対をなすように、レコード型から値を取り出す関数で定義されます。

そしてレコードの値としての実体は、各取り出し関数に対応した値の集まりです。

バリアントの値としての実体

通常バリアントの値は、コンストラクタに対応したラベル付きの値を持つものとして理解されます。 しかし、先ほどのレコードとの双対性を考えると、

  • レコード
    • 型:値を取り出す関数の集まり
    • 値:ラベルに対応した値の集まり
  • バリアント
    • 型:値を作る関数(取り出し関数と対をなす)の集まり
    • 値:ラベルに対応した「値と対を成す何か」の集まり

とするのが綺麗です。 ここで「値と対を成す何か」とは、何なんだって感じですが、これは要は継続です。 つまりバリアントは継続のレコードです。

でも、継続は普通の言語ではCPS変換(全域に渡る)しないとうまく扱えません。 そこで、限定的に何とかしようとすると、値を取り出し同じ型で返すようにして、その後は普通の値として扱えるようにする事になります。

これはまさにパターンマッチです。 最初のbleisさんの記事で

探した限りはあまり他で同じようなことをやっているライブラリがなかったのですが、 LangExtは(F#のような)match式をある程度模倣するMatchメソッドを提供しています。

と最後の方にちょっと出てくるMatchメソッドこそが、バリアントの実体だったのです!!!!

結局

C#などでバリアントを作る場合、型はコンストラクタを提供し、値はコンストラクタに対応したMatch式(メソッド)を実装するのが、綺麗だと思います。

というわけで、アイデンティティとなる型を与えると、コンストラクタができて、それで作った値にはMatchがある、というものを作ってみたらできたのがGenericVariantです。

ただしこれはあくまで、綺麗だと思うかどうかの話であって、綺麗だから実用的だというわけではないので注意が必要です。
例えばこのままだと、あるOption型の値がSomeだとわかっていたとしても、そこから値を取り出すにはMatchを通すしかありません。。。

また、これは先の記事の話題にあった実装の中身を継承で実現するのか、ラベルを持って実装するのかには関係ありません。
今回のGenericVariantでは、SomeやNoneはコンストラクタであって型ではない、またC#ではScalaのsealdにあたるものがないので、勝手に継承されるのを防げないために、継承ではなくラベルを持つ形で実装していますが、
以前作ったOption、Eitherでは継承を使って、Matchメソッドを持つ型であるとしていました。

それと

GenericVariantを作るにあたって、型の数が異なるVariantを作らなければならなかったのでT4を使ってみました。

とか言われてしまいましたが、これくらいなら許される…よね?