F# で lex/yacc っぽいものを作っていると判別共用体(Discriminated Unions)が便利なので、よく使うのでうのですが「C# で使う時はどうするのか?」と聞かれて、はて?と思ってしまいました。Xamarin.Forms のパーサを作っているときは、そのあたりは微妙に避けて(よくわからなかったので)C# からは直接判別共用体を使わないような工夫をしています。
が、Optional を使ったり Choice を使ったりすると(私自身は使ってないけど)、このあたりはどうするのか?って話です。
判別共用体を作る
他の場合も一緒だと思うので、元ネタの判別共用体を作ります。C/C++ で言えば union なので、そんなに違和感はありません。
1 2 3 4 | type A = | STR of string | INT of int | KEYVALUE of key:string * value:string |
判別共用体を使う
判別共用体が便利なのは、利用するときに match で判断できるところです。C/C++ の union の場合は type を内部に持たせるだろうし、Variant の場合も似たり寄ったりです。メモリ効率はどうなのかは気になるところですが、そのあたりは .NET なので、別々に持っているのかも。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 します。
1 2 3 4 5 6 7 8 9 | 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 メソッドを使えばよいことになります。
1 | var a = A.create( "masuda" ); |
F# の match の部分はどうなるかというと、結構面倒です。
これは、判別共用体 A クラスの内容をコンソールに出力しているだけなのですが、IsSTR プロパティでチェックをした後に、A.STR でキャストしないといけません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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# に公開するキャストメソッドを作る
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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 のように明示的にキャストしたときと同じになります。
1 2 | var a = A.create( "masuda" ); string aa = ( string )a; |
いちいち、IsSTR でチェックしなくてよいので、これでよさそうですね。先の op_Explicit メソッドでは string 以外のときは “” で空文字列を返していますが、failwith にして例外にしてもよいでしょう。
KeyValue のような Tuple を返す時には、F# のものを返しても良いのですが、System.Tuple に書き直しています。このため、キャスト自体が面倒で、下記なことになります。
1 2 3 | 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) を付けるとコンパイルエラーになる、という不思議なことになります。
1 2 | // 暗黙のキャストを使う string aa = a; |
これはこれで、var が使えなくなるので不便な感じですよね。また、op_Explicit と op_Implict は同時に定義できないので、どちらか一方だけを使うことになります。これも変な仕様ですが、まあ、そのあたりも含めて注意して作ると、うまく C# で使える判別共用体が作れます。