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

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# で使える判別共用体が作れます。

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