F# で lex/yacc っぽいものを作っていると判別共用体(Discriminated Unions)が便利なので、よく使うのでうのですが「C# で使う時はどうするのか?」と聞かれて、はて?と思ってしまいました。Xamarin.Forms のパーサを作っているときは、そのあたりは微妙に避けて(よくわからなかったので)C# からは直接判別共用体を使わないような工夫をしています。
が、Optional を使ったり Choice を使ったりすると(私自身は使ってないけど)、このあたりはどうするのか?って話です。
判別共用体を作る
他の場合も一緒だと思うので、元ネタの判別共用体を作ります。C/C++ で言えば union なので、そんなに違和感はありません。
type A = | STR of string | INT of int | KEYVALUE of key:string * value:string
判別共用体を使う
判別共用体が便利なのは、利用するときに match で判断できるところです。C/C++ の union の場合は type を内部に持たせるだろうし、Variant の場合も似たり寄ったりです。メモリ効率はどうなのかは気になるところですが、そのあたりは .NET なので、別々に持っているのかも。
let go() = let message a = let msg = match a with | A.STR(s) -> "string is " + s | A.INT(i) -> "int is " + i.ToString() | A.KEYVALUE(k,v) -> "pair is " + k + ", " + v printfn "%A" msg let a = A.STR("masuda") message a let b = A.KEYVALUE("address","itabashi") message b
オブジェクトを作るときは、A.STR あるいは STR で作れて、チェックするときは match で判別します。便利ですね。
これがどのくらい便利かというと、C# に書いてみるとわかります。
C#で判別共用対を使う
まず、A.STR の場合がそのまま使えないので、create のためのメソッドを作っておきます。
Factory パターンで、create します。
type A = | STR of string | INT of int | KEYVALUE of key:string * value:string // C# のための create static member create(s:string) = STR(s) static member create(i:int) = INT(i) static member create(k:string, v:string) = KEYVALUE(k,v)
これで、C# から作成するときは、create メソッドを使えばよいことになります。
var a = A.create("masuda");
F# の match の部分はどうなるかというと、結構面倒です。
これは、判別共用体 A クラスの内容をコンソールに出力しているだけなのですが、IsSTR プロパティでチェックをした後に、A.STR でキャストしないといけません。
static void debug( DiscrUnion.A a ) { if (a.IsSTR) { string s = (a as A.STR).Item; Console.WriteLine("string is {0}", s); } else if (a.IsINT) { int i = (a as A.INT).Item; Console.WriteLine("int is {0}", i); } else if ( a.IsKEYVALUE ) { string k = (a as A.KEYVALUE).key; string v = (a as A.KEYVALUE).value; Console.WriteLine("pair is {0}, {1}", k, v); } else { Console.WriteLine("error"); } }
F# の match に比べるとかなり冗長な感じです。
これが面倒なので、C# に判別共用体クラスを公開するのはやめていたのですが、どうしても公開する必要があるならば、キャストを作ればよかろう、ってことに先日気づきました。
F# でも op_Explicit や op_Implicit メソッドを書くことによって、明示的なキャストと暗黙のキャストを作ることができます。
C# に公開するキャストメソッドを作る
type A = | STR of string | INT of int | KEYVALUE of key:string * value:string // C# のための create static member create(s:string) = STR(s) static member create(i:int) = INT(i) static member create(k:string, v:string) = KEYVALUE(k,v) // C# のための明示的なキャスト用 static member op_Explicit (a:A) : string = match a with | A.STR(s) -> s | _ -> "" static member op_Explicit (a:A) : int = match a with | A.INT(i) -> i | _ -> 0 static member op_Explicit (a:A) : System.Tuple<string,string> = match a with | A.KEYVALUE(k,v) -> new System.Tuple<string,string>(k,v) | _ -> new System.Tuple<string,string>("","") // こっちのほうが便利 member this.ToKeyValue(): System.Tuple<string,string> = match this with | A.KEYVALUE(k,v) -> new System.Tuple<string,string>(k,v) | _ -> new System.Tuple<string,string>("","")
op_Explicit メソッドを作っておくと C# で (string)a のように明示的にキャストしたときと同じになります。
var a = A.create("masuda"); string aa = (string)a;
いちいち、IsSTR でチェックしなくてよいので、これでよさそうですね。先の op_Explicit メソッドでは string 以外のときは “” で空文字列を返していますが、failwith にして例外にしてもよいでしょう。
KeyValue のような Tuple を返す時には、F# のものを返しても良いのですが、System.Tuple に書き直しています。このため、キャスト自体が面倒で、下記なことになります。
var b = A.create("name", "tomoaki"); var bb = (Tuple<string, string>)b; Console.WriteLine("bb is tuple {0}, {1}", bb.Item1, bb.Item2);
型推論の var で受ければよいのでは?と思うところですが、var で受けてしまうと判別共用体の A クラスのままになるので都合が悪いのです。
こういう場合は、素直に ToKeyValue メソッドを作って、インテリセンスを効かせたほうが分かりやすいでしょう。
as ではキャストできない
実は、ひとつ落とし穴があって、a as string のようなキャストはできません。変数 a は A クラスなので、as で Type チェックをすると A 自身が返ってくるためです。なので、(string)a のように「明示的にキャスト」をしないといけないという罠に陥ります。このあたりは、ローカルルールということで、要注意です。
あと、暗黙のキャストを作ろうとすると「暗黙のキャスト」以外ができなくなります。先の op_Explicit を op_Implict に書き換えると、暗黙のキャストになって (string) 部分が要らなくて便利そうなのですが、逆に (string) を付けるとコンパイルエラーになる、という不思議なことになります。
// 暗黙のキャストを使う string aa = a;
これはこれで、var が使えなくなるので不便な感じですよね。また、op_Explicit と op_Implict は同時に定義できないので、どちらか一方だけを使うことになります。これも変な仕様ですが、まあ、そのあたりも含めて注意して作ると、うまく C# で使える判別共用体が作れます。