F# の判別共用体を C# で使いやすいようにキャストする

F# で lex/yacc っぽいものを作っていると判別共用体(Discriminated Unions)が便利なので、よく使うのでうのですが「C# で使う時はどうするのか?」と聞かれて、はて?と思ってしまいました。Xamarin.Forms のパーサを作っているときは、そのあたりは微妙に避けて(よくわからなかったので)C# からは直接判別共用体を使わないような工夫をしています。

が、Optional を使ったり Choice を使ったりすると(私自身は使ってないけど)、このあたりはどうするのか?って話です。

判別共用体を作る

他の場合も一緒だと思うので、元ネタの判別共用体を作ります。C/C++ で言えば union なので、そんなに違和感はありません。

[code lang=”vb”]
type A =
| STR of string
| INT of int
| KEYVALUE of key:string * value:string
[/code]

判別共用体を使う

判別共用体が便利なのは、利用するときに match で判断できるところです。C/C++ の union の場合は type を内部に持たせるだろうし、Variant の場合も似たり寄ったりです。メモリ効率はどうなのかは気になるところですが、そのあたりは .NET なので、別々に持っているのかも。

[code lang=”vb”]
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
[/code]

オブジェクトを作るときは、A.STR あるいは STR で作れて、チェックするときは match で判別します。便利ですね。
これがどのくらい便利かというと、C# に書いてみるとわかります。

C#で判別共用対を使う

まず、A.STR の場合がそのまま使えないので、create のためのメソッドを作っておきます。
Factory パターンで、create します。

[code lang=”vb”]
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)
[/code]

これで、C# から作成するときは、create メソッドを使えばよいことになります。

[code lang=”csharp”]
var a = A.create("masuda");
[/code]

F# の match の部分はどうなるかというと、結構面倒です。
これは、判別共用体 A クラスの内容をコンソールに出力しているだけなのですが、IsSTR プロパティでチェックをした後に、A.STR でキャストしないといけません。

[code lang=”csharp”]
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");
}
}
[/code]

F# の match に比べるとかなり冗長な感じです。

これが面倒なので、C# に判別共用体クラスを公開するのはやめていたのですが、どうしても公開する必要があるならば、キャストを作ればよかろう、ってことに先日気づきました。
F# でも op_Explicit や op_Implicit メソッドを書くことによって、明示的なキャストと暗黙のキャストを作ることができます。

C# に公開するキャストメソッドを作る

[code lang=”vb”]
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>("","")
[/code]

op_Explicit メソッドを作っておくと C# で (string)a のように明示的にキャストしたときと同じになります。

[code lang=”csharp”]
var a = A.create("masuda");
string aa = (string)a;
[/code]

いちいち、IsSTR でチェックしなくてよいので、これでよさそうですね。先の op_Explicit メソッドでは string 以外のときは “” で空文字列を返していますが、failwith にして例外にしてもよいでしょう。

KeyValue のような Tuple を返す時には、F# のものを返しても良いのですが、System.Tuple に書き直しています。このため、キャスト自体が面倒で、下記なことになります。

[code lang=”csharp”]
var b = A.create("name", "tomoaki");
var bb = (Tuple<string, string>)b;
Console.WriteLine("bb is tuple {0}, {1}", bb.Item1, bb.Item2);
[/code]

型推論の var で受ければよいのでは?と思うところですが、var で受けてしまうと判別共用体の A クラスのままになるので都合が悪いのです。
こういう場合は、素直に ToKeyValue メソッドを作って、インテリセンスを効かせたほうが分かりやすいでしょう。

as ではキャストできない

実は、ひとつ落とし穴があって、a as string のようなキャストはできません。変数 a は A クラスなので、as で Type チェックをすると A 自身が返ってくるためです。なので、(string)a のように「明示的にキャスト」をしないといけないという罠に陥ります。このあたりは、ローカルルールということで、要注意です。

あと、暗黙のキャストを作ろうとすると「暗黙のキャスト」以外ができなくなります。先の op_Explicit を op_Implict に書き換えると、暗黙のキャストになって (string) 部分が要らなくて便利そうなのですが、逆に (string) を付けるとコンパイルエラーになる、という不思議なことになります。

[code lang=”csharp”]
// 暗黙のキャストを使う
string aa = a;
[/code]

これはこれで、var が使えなくなるので不便な感じですよね。また、op_Explicit と op_Implict は同時に定義できないので、どちらか一方だけを使うことになります。これも変な仕様ですが、まあ、そのあたりも含めて注意して作ると、うまく C# で使える判別共用体が作れます。

カテゴリー: C#, F# パーマリンク