tonari note

オンラインゲームエンジニアの雑記

C#でパターンマッチ


ひとりごと - tonari note

前回、独り言のようにつぶやいたこれですが、一旦形になった気がするのでまとめます。
テストとか超適当。
あとUnityでJIT落ちしない事は一応確認していますが、もし落ちるとかあったら教えてくださいませ。


yKimisaki/ActionPattern · GitHub

パターンマッチって何ぞや?って方はコチラへ。

パターン マッチ (F#)

そもそも最初に、ちょっとした分岐でswitchとかifとか書きたくない、null返ってきても良い感じに処理したい、みたいな事がありまして作る経緯となりました。

まずF#なパターンマッチのサンプルの

type Color =
    | Red = 0
    | Green = 1
    | Blue = 2

let printColorName (color:Color) =
    match color with
    | Color.Red -> printfn "Red"
    | Color.Green -> printfn "Green"
    | Color.Blue -> printfn "Blue"
    | _ -> ()

は今回のライブラリを使うと

enum Color
{
    Red = 0,
    Green = 1,
    Blue = 2
}

 // 型引数は、Color型でマッチするActionPatternですよ、という意味
var printColorName = ActionPattern<Color>
    .Pattern(Color.Red, x => Console.WriteLine("Red"))
    .Pattern(Color.Green, x => Console.WriteLine("Green"))
    .Pattern(Color.Blue, x => Console.WriteLine("Blue"))
    .Default(x => { });

となります。
パターンは上から順番にマッチするか確かめ、最初にマッチしたパターンのラムダ式を呼び出すインターフェイスを返します。
後のラムダ式のxにはマッチした値が入っています。
当てはまらなかった場合はDefaultが呼ばれます。

Defaultは必ず設定しなくてもコンパイルできますが、どのパターンにも当てはまらず、Defaultもないパターンを呼び出すと例外を投げるので注意してください。
まぁこの辺はC#の限界って所かなぁ。

また、F#のnullのマッチも同じように、

var printColorName = ActionPattern<Color>
    .Pattern(Color.Red, x => Console.WriteLine("Red"))
    .CatchNull(x => { })
    .Default(x => { });

とCatchNullで設定でき、nullが来たときはこちらが呼ばれます。
また、CatchNullがない時にnullが放り込まれると、ActionPatternはDefaultを呼ぼうとします。

作成したパターンを実際に使用する場合は、Match拡張メソッドを使用します。

Color.Red.Match(printColorName);
// Console.WriteLine("Red")) が呼ばれる 

ActionPatternのマッチ判定部分には、boolを返すラムダ式も使うことが出来ます。

var fizzbuzz = ActionPattern<int>
    .Pattern(x => x % 15 == 0, x => Console.WriteLine("fizzbuzz"))
    .Pattern(x => x % 5 == 0, x => Console.WriteLine("buzz"))
    .Pattern(x => x % 3 == 0, x => Console.WriteLine("fizz"))
    .Default(x => Console.WriteLine(x.ToString()));

このように多少複雑なマッチングも対応しています。

ActionPatternはIEnumerableに対する拡張メソッドを用意しています。
上のfizzbuzzを例にすると、

Enumerable.Range(1, 100).MatchTrace(fizzbuzz).ToArray();

でIEnumerableの各要素に対して順番にパターンマッチを適応させていきます。
MatchTraceの返り血はIEnumerableで、Actionを実行したあと、シーケンスをそのまま返します。
あと遅延評価なので今回はToArray()して無理やり呼んでいます。

ActionPatternは戻り値をとることが出来ます。

 // 型引数:Colorでマッチ、stringを返す
var getColorName = ActionPattern<Color, string>
    .Pattern(Color.Red, x => "Red")
    .Pattern(Color.Green, x => "Green")
    .Pattern(Color.Blue, x => "Blue")
    .Default(x => "");

var redColorName = Color.Red.Match(getColorName);

返り値を取るActionPatternのLINQ拡張メソッドはMatchSelectです。
Selectの様にパターンマッチすることにより、シーケンスの型が変わります。

最後に、Match時に値を渡したい、という時のために、1引数までなら今のところ対応しています。

 // 型引数:intでマッチ、stringを追加の引数にし、stringを返す
var getClampedScore = ActionPattern<int, string, string>
    .Pattern(x => x < 0, (x, format) => 0.ToString(format))    
    .Pattern(x => x > 100, (x, format) => 100.ToString(format))
    .Default((x, format) => x.ToString(format);

var score = 56.Match(clamp, "000");

このようにMatchの第二引数に追加の引数を渡します。
また、マッチ部分でも第二引数を用いて評価することができます。

var hogehoge = ActionPattern<int, string, string>
    .Pattern((x, format) => x < 0, (x, format) => 0.ToString(format));

自分のやりたいことはできるようになったぞい!
やったね。