ツイッターのTL見ていると、徐々にC# 7の話題が出てきています。C# 7でパターンマッチングが導入されるとか(まだ範囲は確定してません)。そんなわけで、「パターンマッチング」で面白いと感じた事を書いてみます。
C# 7で導入が検討されている機能については、岩永さんの記事が非常にわかりやすくて良いと思います。是非参照してみてください。
私の場合、時期が微妙にずれているのですが、C# 7ではなくF#でパターンマッチングに触れました。その前にはScalaでほんの少しだけ触れたのですが、結局実用的なコードをほとんど書かなかったこともあって、モノにもならなければその良さもわからず仕舞いでした。さらにその前では、某名古屋のシャチョーさんにHaxeで判別共用体を使う例をちょっとだけ紹介してもらった事がある程度です。
F#は業務で使っていることもあり(但し、まだ使いこなしているとは言い難い)、F#に限ってですが、ようやくその利点が頭の中で整理されてきた感があります。「パターンマッチング」の何が良いのか、面白いのか、という事を明らかに出来たらいいなーと思っています。
# サンプルコードはそのままではコンパイルできない式があったりしますが、説明を優先します。また、F#らしからぬコード(例: printfn使ってない)がありますが、対比を優先させています。加えて、多少冗長に書いている箇所があります。
switch-caseの代替としてのパターンマッチング
パターンマッチングの最初の導入は、単にif-then-elseやswitch-caseの置き換えと言うものです。その前にswitch-caseの導入を(ばかばかしいですが)しておきましょう。
var value = 102;
// ifで分岐
if (value == 100)
{
// 100に対応する処理
}
else if (value == 101)
{
// 101に対応する処理
}
else if (value == 102)
{
// 102に対応する処理
}
else if (value == 103)
{
// 103に対応する処理
}
else
{
// それ以外の値の処理
}
これが:
var value = 102;
// switchで分岐
switch (value)
{
case 100:
// 100に対応する処理
break;
case 101:
// 101に対応する処理
break;
case 102:
// 102に対応する処理
break;
case 103:
// 103に対応する処理
break;
default:
// それ以外の値の処理
break;
}
こうなりますね。利点としては、判定される対応する値が「case」句によって明示される(のでわかりやすい)という事でしょうか。また、case句には定数しか置けないので、複雑な判定条件が入る余地がないという事もあります。私はこれを「宣言的」だと感じています。デメリットとして、それがそのまま欠点となり、caseに判定式が書けないという事になります。
C# 1(1ですよ)が出た当時、Javaとの比較において、判定対象に文字列が使えることがアドバンテージだと言われていました。
var value = "CCC";
// switchで分岐
switch (value)
{
case "AAA":
// "AAA"に対応する処理
break;
case "BBB":
// "BBB"に対応する処理
break;
case "CCC":
// "CCC"に対応する処理
break;
case "DDD":
// "DDD"に対応する処理
break;
default:
// それ以外の値の処理
break;
}
私は文字列をswitch-caseの判定に使えるからと言って、それが大きな利点だとは思えませんでした。むしろ、判定対象の値がプリミティブ型と文字列だけではなく、それ以外の型のインスタンスが指定されたら、EqualsやIEquatable<T>を使って判定してくれればもっと使い出があるのに、と思ってました(caseにどのようにインスタンスを指定するのかという課題はありますが…)。このあたりのモヤモヤにも、あとで取り組みます。
さて、switch-caseと同じことをF#のパターンマッチングでやってみます。
let value = 102
// matchでパターンマッチング
match value with
| 100 ->
// 100に対応する処理
| 101 ->
// 101に対応する処理
| 102 ->
// 102に対応する処理
| 103 ->
// 103に対応する処理
| _ ->
// それ以外の値の処理
F#を書いたことがない人でもだいたい読めると思います。switch-caseと比較してもほぼ一対一に置き換わっている感じがしますね。
最初のポイントは、match-withを使う場合は「それ以外の処理」を省略できない事です。F#ではすべての式(上記では処理)が戻り値を持つことになっているので、特定のマッチが値を返さないと矛盾してしまいます。最後のアンダースコアを使う “_ -> …” というマッチで、その他の値が担保されます。これによって、valueに対する評価が「網羅される」(条件に抜けがない)ことがコンパイラによって保証されます(漏れていると警告が発生します。目的を考えるとバグと思われるので、警告をエラーとするコンパイルスイッチを有効化しても良いかもしれません)。
直値を避けることと、その判定
上記に挙げたような「直値」による判定は、出来るだけ避けた方が良いというのが一般的な指針でしょう。簡単なミスですが、例えば”100″とタイプするところを”10″とタイプしてしまってこれに気が付かないというようなバグを生んでしまう可能性があります。C/C++言語であれば、#defineによるプリプロセッサを使って置き換えることを考えると思います。或いは(C#でも)enum型をつかえば、直値をシンボリックな値に置き換える事が出来ます。そして、enum値はswitch-caseでも使えます。
# 抽象型を導入して解決すべきという意見もあるかもしれませんが、その話はまた後で。
// enum型を宣言する
public enum Modes
{
LightMode = 100,
StandardMode = 101,
ManualMode = 102,
CatMode = 103
}
var value = Modes.StandardMode;
// switchで分岐
switch (value)
{
case Modes.LightMode:
// LightModeに対応する処理
break;
case Modes.StandardMode:
// StandardModeに対応する処理
break;
case Modes.ManualMode:
// ManualModeに対応する処理
break;
case Modes.CatMode:
// CatModeに対応する処理
break;
}
良い感じです。普段はシンボル名を使うとして、実際の直値”100″をLightModeに対応付ける方法は考える必要があります。C#の場合は、上記のようにenumの各値にintの値を対応付けることもできるので、これを使うと実装が楽になると思います。
同じようにこれをF#で書きます:
// enum型を宣言する(数値は省略)
type Modes = LightMode | StandardMode | ManualMode | CatMode
let value = Modes.StandardMode
// matchでパターンマッチング
match value with
| Modes.LightMode ->
// LightModeに対応する処理
| Modes.StandardMode ->
// StandardModeに対応する処理
| Modes.ManualMode ->
// ManualModeに対応する処理
| Modes.CatMode ->
// CatModeに対応する処理
match-with式が値の網羅性を担保するため、enum型の値4つのパターンが網羅されていればコンパイルに成功します。逆に足りない場合は警告が発生するので、うっかり記述漏れを起こすということがありません。逆に「該当しない」場合を泥縄的に処理する場合は、defaultと同じように書く事が出来ます:
match value with
| Modes.LightMode ->
// LightModeに対応する処理
| Modes.CatMode ->
// CatModeに対応する処理
| _ ->
// それ以外の値をすべて処理
F#が、式が完全である事を要求するこだわりは、以下の例でよくわかります。
// enum型を宣言する(対応する数値を明示する)
type Modes = LightMode = 100 | StandardMode = 101 | ManualMode = 102 | CatMode = 103
let value = Modes.StandardMode
// "警告 FS0025: この式のパターン マッチが不完全です
// たとえば、値 'enum<Modes> (0)' はパターンに含まれないケースを示す可能性があります。"
match value with
| Modes.LightMode ->
// LightModeに対応する処理
| Modes.StandardMode ->
// StandardModeに対応する処理
| Modes.ManualMode->
// ManualModeに対応する処理
| Modes.CatMode->
// CatModeに対応する処理
まず、enum型の値に数値を明示する場合は、すべての値に明示する必要があります(C#では省略可能で、直前の値がインクリメントされる)。そして、上記のenum値には”0″に対応する値が定義されていないため、「valueが値”0″に相当した場合の処理を網羅していない」という警告が発生します。
詳しい説明は省きますが、enum型はValue-Type(値型)なので、デフォルトの値(C#で言う所のdefault(Modes))となる可能性があり、ここではそのことを警告しています(言語というより、.NET CLRの特性による)。enum型の値に対応する数値を明示的に指定しない場合、enum型の最初の値(上記ではLightMode)が値”0″に対応するので、結果的に警告は発生しません。
switch-caseとパターンマッチングの分かれ目
前節の例のように、F#は「転びそうなミスを拾ってくれ」ます。しかし、今まで見てきた例は、殆ど直接的にif-then-elseやswitch-caseに置き換える事が出来ます。これでは、パターンマッチングという新しい武器に「興味をそそられない」としても、仕方が無いと思います。
しかし、ここからがパターンマッチングの強力さを発揮する部分です。
複数の値の組み合わせに基づいて処理を決定するようなコードを書いたことがあるでしょう。ここにswitch-caseを適用すべきかどうかは、個人によって判断の基準が色々ありそうです。ここではベタに書いてみます。
// enum型の定義は省略
var value1 = Modes1.StandardMode; // (4種類)
var value2 = Modes2.PowerfulMode; // (3種類)
switch (value1)
{
case Modes1.LightMode:
switch (value2)
{
case Modes2.MinimumMode:
// LightModeかつMinimumModeの処理
break;
case Modes2.RichMode:
// LightModeかつRichModeの処理
break;
case Modes2.PowerfulMode:
// LightModeかつPowerfulModeの処理
break;
}
break;
case Modes2.StandardMode:
switch (value2)
{
case Modes2.MinimumMode:
// StandardModeかつMinimumModeの処理
break;
case Modes2.RichMode:
// StandardModeかつRichModeの処理
break;
case Modes2.PowerfulMode:
// StandardModeかつPowerfulModeの処理
break;
}
break;
// (以下、ひたすら続く...)
}
「こんな惨いコード書かねぇ!これならifで書いたほうが良い!!」或いは「抽象化しろ!」って言われそうです。見た目の問題やコードがやたら長くなることも問題ですが、もっと深刻なのは、組み合わせが正しく網羅されているのかどうかを確認するのが大変そうだという事です。
F#で書いてみます:
// enum型の定義は省略
let value1 = Modes1.StandardMode; // (4種類)
let value2 = Modes2.PowerfulMode; // (3種類)
// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
// LightModeかつMinimumModeの処理
| Modes1.LightMode, Modes2.RichMode ->
// LightModeかつRichModeの処理
| Modes1.LightMode, Modes2.PowerfulMode ->
// LightModeかつPowerfulModeの処理
| Modes1.StandardMode, Modes2.MinimumMode ->
// StandardModeかつMinimumModeの処理
| Modes1.StandardMode, Modes2.RichMode ->
// StandardModeかつRichModeの処理
| Modes1.StandardMode, Modes2.PowerfulMode ->
// StandardModeかつPowerfulModeの処理
| ... (以下、マッチ式が続くが、組み合わせが網羅されてないと警告が出る)
私は初見で、C#にもこれ欲しい!!と思いました。或いは、これならネストしていた方が良いと思う人もいるかもしれません(matchをネストさせることもできますが省略)。
しかし、パターンマッチングはもっともっと強力です。面白いのは、組み合わせ網羅を全部書かなくても良いことです:
// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
// LightModeかつMinimumModeの処理
| Modes1.LightMode, Modes2.RichMode ->
// LightModeかつRichModeの処理
| Modes1.LightMode, Modes2.PowerfulMode ->
// LightModeかつPowerfulModeの処理
| _, Modes2.MinimumMode ->
// 残りのMinimumModeの処理
| _, Modes2.RichMode ->
// 残りのRichModeの処理
| _, Modes2.PowerfulMode ->
// 残りのPowerfulModeの処理
// (これですべて網羅された)
これはつまり、value1がLightModeの時だけ、value2に応じた個別の処理を行い、value1がそれ以外の値の場合はvalue2に応じた共通かつ個別の処理を行うという事です。これを見て、コードがより「宣言的」に近づいたと思いませんか? 実際、F#コンパイラはこれらの網羅に漏れがないかどうかをチェックしています。さらに複雑な例を見てみます:
// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
// LightModeかつMinimumModeの処理
| Modes1.LightMode, Modes2.RichMode ->
// LightModeかつRichModeの処理
| Modes1.LightMode, Modes2.PowerfulMode ->
// LightModeかつPowerfulModeの処理
| Modes1.CatMode, Modes2.RichMode ->
// CatModeかつRichModeの処理
| _, Modes2.MinimumMode ->
// 残りのMinimumModeの処理
| _, Modes2.PowerfulMode ->
// 残りのPowerfulModeの処理
| _, _ ->
// 残りのすべての処理
「残りのすべての処理」が無いと警告が発生します。何が該当するかは、実際に考えてみて下さい。
もちろん、match式の組み合わせ対象の値は、更に3個4個…n個とカンマで区切って指定できます。複雑にネストしてしまうようなif-thenやswitch-caseよりもスマートに書ける上に、メタプログラミング(T4のようなコード生成)でmatch式を動的に生成する場合にも重宝しそうです。
タプルとパターンマッチングの関係
前述の例で、Modes1の残りの値の組をデフォルト処理しましたが、具体的に値が何であったのかを知る方法があります。
// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
// LightModeかつMinimumModeの処理
| Modes1.LightMode, Modes2.RichMode ->
// LightModeかつRichModeの処理
| Modes1.LightMode, Modes2.PowerfulMode ->
// LightModeかつPowerfulModeの処理
| v1, Modes2.MinimumMode ->
// (v1からModes1の実際の値が得られる)
| _, _ ->
// 残りのすべての処理
アンダースコアの代わりにシンボル(ここではv1)を指定することで、対応する値が何であったのかを得る事が出来ます。value2はマッチ式からMinimumModeであることが分かっているので、value2については不要ですね。この例ではv1とはつまりvalue1の事なので、値が取得できたところで意味が無い(value1でアクセスすればよい)ように見えます。この構文が生かされるのはもう少し後です。
所で、マッチ式を以下のようにして、結果をまとめて取得する方法もあります。
// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
// ...
| Modes1.LightMode, Modes2.RichMode ->
// ...
| Modes1.LightMode, Modes2.PowerfulMode ->
// ...
| _, _ as pair ->
// (pairで両方の値にアクセスできる)
「as」演算子を使うと、パターンマッチングの結果が参照できるようになります。では、上記の「pair」で何が参照出来るのでしょうか? 実はpairは「タプル」になります。F#のタプルは、内部的にはSystem.Tuple<…>で定義されているのですが、Item1、Item2のようなプロパティは参照できず、その必要もありません。
タプルの値を分解するには、以下のようにletを使います:
// 組み合わせをパターンマッチングで評価する
match value1, value2 with
// ...
| _, _ as pair ->
let v1, v2 = pair // v1とv2にそれぞれItem1とItem2の値が入る
System.Console.WriteLine("{0}, {1}", v1, v2) // 普通に使える
v1とv2はもちろん型推論で自動的にModes1型とModes2型として扱われます。ところでこの式を見ていて、マッチ式がモヤモヤしてきませんか?
// 組み合わせをパターンマッチングで評価する
match value1, value2 with
// ...
| v1, v2 -> // letで分解しなくても、実はこれでいい?
// let v1, v2 = pair // v1とv2にそれぞれItem1とItem2の値が入る
System.Console.WriteLine("{0}, {1}", v1, v2)
そして:
// タプルを作る
let pair = (value1, value2)
// それって実はこうじゃね?
match pair with
| Modes1.LightMode, Modes2.MinimumMode ->
// ...
| Modes1.LightMode, Modes2.RichMode ->
// ...
| Modes1.LightMode, Modes2.PowerfulMode ->
// ...
| v1, v2 -> // letで分解しなくても、実はこれでいい?
// let v1, v2 = pair // v1とv2にそれぞれItem1とItem2の値が入る
System.Console.WriteLine("{0}, {1}", v1, v2)
実は “match value1, value2 with” という式は、組み合わせマッチングのための特別な構文ではなく、その場でvalue1とvalue2のタプルを作って渡しているだけと見る事が出来ます。更にマッチ式のカンマで区切った構文も特別な構文ではなく、タプルの各要素にマッチングしているだけなのです!!
そういうわけで、パターンマッチングを書いている場合は、実はあまりタプルの事は意識していませんが、内部では非常に多くの場面でタプルが自動的に使われています。
この例でもまだvalue1とvalue2にはアクセスできるので、v1、v2に分解する事に意味を見いだせないかもしれません。これならどうでしょうか:
// 関数化(引数はタプル)
let applyPair p =
// タプルから直接パターンマッチングする
match p with
| Modes1.LightMode, Modes2.MinimumMode ->
// ...
| Modes1.LightMode, Modes2.RichMode ->
// ...
| Modes1.LightMode, Modes2.PowerfulMode ->
// ...
| v1, v2 -> // マッチ式で直接タプルを分解
System.Console.WriteLine("{0}, {1}", v1, v2)
// タプルを作る
let pair = (Modes1.StandardMode, Modes2.PowerfulMode)
// 関数呼び出し
applyPair pair
もはやapplyPairは独立した関数なので、内部のマッチ式で直接value1、value2にはアクセスできません。それどころかタプルを直接生成しているので、value1もvalue2も定義されていません。
関数の引数pは型が推測され、Modes1とModes2のタプルとなります。そしてマッチ式によって判定とタプルの分解が一度に行われます。これに慣れてくると、Tuple<…>ほにゃほにゃとか、Item1、Item2のようにプロパティにいちいちアクセスするのが、とても面倒になってきます。また、C#でTupleを使うと意味が不明瞭になるため乱用しない方が良いというセオリーがありますが、F#のタプルではその点で困ることはあまりありません。
シーケンスのマッチング
シーケンスとは、IEnumerable<T>の事です。つまりC#で言う所のLINQ可能なインスタンスです。F#の世界では、手軽に扱えるシーケンスが2種類あります。「リスト」と「配列」です。F#配列は、System.Arrayの派生クラスなのでC#と同じですが、F#のリストは.NETの「System.Collections.Generic.List<T>」クラスではなく、読み取り専用の順方向リンクリストとして実装されたクラスです。これを使うと、シーケンスの各要素をパターンマッチングでマッチさせながら分解までやらせることが出来ます。
// F#リストを手動で定義
let values = [102; 107; 111; 133; 50]
// F#リストをパターンマッチングする
match values with
| [] ->
// 空のリストにマッチ
System.Console.WriteLine("Empty")
| [x; y; z] ->
// 3つの要素が含まれるリストにマッチし、かつそれぞれの値を参照可能にする
System.Console.WriteLine(
System.String.Format(
"x={0},y={1},z={2}",
x, y, z))
| [a; b; c; d; e] ->
// 5つの要素が含まれるリストにマッチし、かつそれぞれの値を参照可能にする
System.Console.WriteLine(
System.String.Format(
"a={0},b={1},c={2},d={3},e={4}",
a, b, c, d, e))
| xs ->
// その他のリストにマッチし、xsで参照可能にする
System.Console.WriteLine(System.String.Join(",", xs))
この例では、a,b,c,d,eの5つの要素を分解するマッチ式にマッチします。これもベタで書くと面倒なコードになりますが、パターンマッチングで書けばシンプルかつ安全に書くことが出来ます。
配列の構文は “[| … |]” ですが、上記のF#リストの例と全く同じように記述できます。残念ながら「シーケンスそのもの」をパターンマッチングでマッチさせる事はできません。シーケンスを扱う場合は、リストか配列に固定化すれば扱えます。
// F#リストを手動で定義し、シーケンスとして扱う
// 型: seq int (IEnumerable<int>)
let values = seq [102; 107; 111; 133; 50]
// シーケンスをF#リストに変換してマッチさせる
match values1 |> Seq.toList with
| [] ->
// ...
| [x; y; z] ->
// ...
| [a; b; c; d; e] ->
// ...
| xs ->
// ...
ところで、パターンマッチングで使える道具はほとんど自由に組み合わせて使えます。enum型も使えるし、タプルでもOKです。それらをマッチ式内で分解させると、普通に書くには面倒すぎる操作も自由自在です。組み合わせた例を示します:
// enum型を宣言する
type Modes = LightMode | StandardMode | ManualMode | CatMode
// 複雑なF#リストを定義する
let values = [
(102, ("ABC", Modes.StandardMode));
(107, ("DEF", Modes.CatMode));
(111, ("GHI", Modes.LightMode));
(133, ("JKL", Modes.StandardMode));
(50, ("MNO", Modes.ManualMode))
]
// F#リストを要素毎に定義されたマッチ式でマッチさせる
match values with
| [] ->
System.Console.WriteLine("Empty")
| [(number0, (name0, mode0)); (number1, (name1, mode1))] ->
// 2つの要素が含まれるリストにマッチし、内容をすべて分解して参照可能にする
System.Console.WriteLine(
System.String.Format(
"Number={0},Name={1},Mode={2}",
number0, name0, mode0))
System.Console.WriteLine(
System.String.Format(
"Number={0},Name={1},Mode={2}",
number1, name1, mode1))
| [(number0, (name0, mode0)); _; _; _; (number1, (name1, mode1))] ->
// 5つの要素が含まれるリストにマッチし、最初と最後の要素だけを分解して参照可能にする
System.Console.WriteLine(
System.String.Format(
"Number={0},Name={1},Mode={2}",
number0, name0, mode0))
System.Console.WriteLine(
System.String.Format(
"Number={0},Name={1},Mode={2}",
number1, name1, mode1))
| _ ->
// ...
マッチする事自体は判定したいけど、実際にマッチした値が不要なら、アンダースコアを使って取得しない、という選択も出来ますよ。そしてもちろん、タプルの内側の特定の値だけ不要な場合とか、全く同じように書けます(例えばname1が不要なら、”(number1, (_, model1))”)。
「うわ、何これ…」みたいに感じてきましたか? まだまだこれからです。
実行時の型でマッチングする
例えば、与えられる値の種類に応じて、それぞれの処理を切り替えたいという要求はよくあると思います。XML DOMのノードが良い例です。XMLのノードは、エレメント・テキスト・XML属性などで構成されていますが、トラバースする場合は同じ「ノード」として扱い、それぞれの場合に応じて処理を変更する必要があります。
using System.Xml.Linq;
// 指定されたノードを解析して結果を取得する再帰メソッド
public static string TraverseNode(XNode node)
{
// ノードがエレメントなら
var element = node as XElement;
if (element != null)
{
// 子ノードも探索する
return string.Format(
"Element: {0} {{ {1} }}",
element.Name,
string.Join(", ", element.Elements().Select(child => TraverseNode(child))));
}
// ノードがテキストなら
var text = node as XText;
if (text != null)
{
return string.Format(
"Text: \"{0}\"",
text.Value);
}
// (他は省略)
return "(Unknown)";
}
ここで、switch-caseが使えれば良いのですが、caseには直値を指定しなければならないため、switch-caseは使えません。同じような処理をF#で書いてみます:
open System.Xml.Linq
// 指定されたノードを解析して結果を取得する再帰関数(引数に型注釈が必要)
let rec traverseNode (node: XNode) =
match node with
// ノードがエレメントなら
| :? XElement as element ->
System.String.Format(
"Element: {0} {{ {1} }}",
element.Name,
System.String.Join(", ", (element.Elements() |> Seq.map (fun child -> traverseNode child))))
// ノードがテキストなら
| :? XText as text ->
System.String.Format(
"Text: \"{0}\"",
text.Value)
// (他は省略)
| _ -> "Unknown"
# 型注釈と言うのは、型を推測できない場合にヒントとして補うことです。この例で型注釈(XNode)が必要な理由については次節で説明します。
マッチ式が “:? XElement as element” となっていますが、これは「指定された値の型がXElementでキャスト可能な場合に、キャストされた値を”element”で参照できるようにする」という意味です。XTextも同様です。このように、switch-caseのような直値だけではなく、柔軟な判定を行う事が出来ます。
マッチ式に “:?” パターンを使う場合は、必ずすべてのマッチ式が “:?” を使わなければならない訳ではありません。有効なマッチ式であればどのようにでも指定する事が出来ます。
言語機能の直交性
前節の例では、.NET標準のXML LINQの型を使ったので、型の動的な判定を行わせています。XElementやXTextはXNodeを継承しているという「特性を利用」し、引数ではXNodeを渡させています。引数にXNodeの型注釈が必要なのは、クラスの継承やインターフェイスの実装を考えると、F#だけで型を推測することが出来ないためです(未知の関連型を網羅検索出来る必要がある)。
そのため、F#だけで書いてよいのなら、OOP的なアプローチを取らず、「判別共用体」という種類の型を使います。
// ノード型(判別共用体)
type Node =
// エレメントの場合は名前と子要素のシーケンスをタプルで保持
| Element of (string * (Node seq))
// テキストの場合は文字列を保持
| Text of string
# タプルの型記法は慣れてないと気持ち悪いかもしれません “(type1 * type2 …)” のようにアスタリスクで要素の型を区切ります。
「共用体」というと、C言語のunionを思い浮かべる人も居るかもしれませんが、意図としては似ています。このコードは”Node”型を定義しますが、これはいわゆる「基底クラス」や「基底インターフェイス」ではありません。内部に定義されている「Element」と「Text」の二つの種類の値(用語でTagと言います)を持ちうる型です。また、この値以外の値が存在しない(未知の値がない)事がはっきりしています。
enum型と比較すると、シンボル名が整数に対応づけされるものではなく、それぞれの値が独自の異なる値の組を持つ事が出来るところが違います。
- Nodeの値が”Element”である場合は “(string * (Node seq))” つまり文字列とNodeのシーケンス(のタプル)を持つ。
- Nodeの値が”Text”である場合は “string” つまり文字列だけを持つ。
# 判別共用体がIL上でどのように実現されているのかについては、ここでは触れません。少なくともILのネイティブ表現に対応するものはありません。
C言語のunionは危険な使い方が出来ますが、判別共用体で危険な使い方は出来ません。例えば、Nodeの値が”Text”であるのに、無理矢理Elementとみなしてアクセスすることは不可能です。逆もまた然り。その安全性の担保は、マッチ式で行われます。
type Node =
| Element of (string * (Node seq))
| Text of string
// 指定されたノードを解析して結果を取得する再帰関数
let rec traverseNode node =
match node with
// ノードがエレメントなら
| Element (name, children) ->
System.String.Format(
"Element: {0} {{ {1} }}",
name,
System.String.Join(", ", (children |> Seq.map (fun child -> traverseNode child))))
// ノードがテキストなら
| Text text ->
System.String.Format(
"Text: \"{0}\"",
// (他の値はとりようがないのですべて網羅された)
このコードには色々と「おいしい」部分が含まれています。まず、関数の引数に型注釈は不要です。どうやってNode型であることを推測しているかというと、マッチ式に”Element”と”Text”が現れ、しかもキャプチャする引数の並びがすべて一致(Elementは2個、Textは1個)し、それぞれの引数の型が、内側の式から推測された型に一致する定義がNode型しか無いと認識できるからです。
そしてマッチ式を見ると、ElementとTextに「関数引数のようなもの」が指定されています。これらが、それぞれの値(Tag)にマッチした時の値として参照可能になります。つまり:
type Node =
| Element of (string * (Node seq)) // (name, children)
| Text of string // text
let rec traverseNode node =
match node with
| Element (name, children) ->
// 判別共用体Element Tagのタプルにマッチし、
// タプルの値がそれぞれ name と children で参照可能となるように分解される
| Text text ->
// 判別共用体Text Tagにマッチし、文字列がtextで参照可能になる
前節でタプルをパターンマッチングで分解する例を見せましたが、ここでも分解が応用されています。もちろん:
let rec traverseNode node =
match node with
| Element pair ->
// 判別共用体Element Tagのタプルにマッチし、pairで参照可能になる
let name, children = pair
// ...
| Text text ->
// 判別共用体Text Tagにマッチし、文字列がtextで参照可能になる
タプルをマッチ式で分解しないで、直接参照可能にすることもできます。
このように、パターンマッチングで使われるこまごまとした機能が直交的に組み合わされて、高い表現力を持つようになっています。前節で示した、enum型・タプルのネスト・シーケンスのマッチングを判別共用体と組み合わせることも可能です。
ネストした判別共用体のマッチング
パターンマッチの機能が直交的だと思える例をもう一つ。ここまでで示した道具がすべて組み合わせ可能と言う事は、判別共用体自身もまた組み合わせ可能と言う事です。ネストした判別共用体の強力なパターンマッチング例をお見せします。
type Value =
| Numeric of int
| Text of string
| Date of System.DateTime
// ネストした判別共用体
type Information =
| Type1 of (string * Value)
| Type2 of string
// Information型の値
let value = Type1 ("ABC", Numeric(123))
// Information型のパターンマッチング
match value with
| Type1 (name, Numeric numeric) ->
System.Console.WriteLine(
"Type1: Name={0}, Numeric={1}",
name,
numeric)
| Type1 (name, Text text) ->
System.Console.WriteLine(
"Type1: Name={0}, Text={1}, Length={2}",
name,
text,
text.Length)
| Type1 (name, Date date) ->
System.Console.WriteLine(
"Type1: Name={0}, Year={1}, Month={2}, Day={3}",
name,
date.Year,
date.Month,
date.Day)
| Type2 text ->
System.Console.WriteLine(
"Type2: Text={0}",
text)
もちろん、いくらでもネスト可能で、ネストした細部の値を直接分解して参照することが出来ます。パターンマッチング無しで同じ処理を実現しようと思うと、不可能ではありませんが(大した処理ではないにも関わらず)非常に面倒でミスも起きやすいコードになると思います。
型のメンバーにパターンマッチングしたい
ここまでマッチングが柔軟であると、クラスのメンバ、例えばプロパティに対してもパターンマッチングしたいと考えるかもしれません。
// クラス型の定義(引数はコンストラクタの引数)
type DemoClass (firstName: string, lastName: string, age: int) =
// コンストラクタ引数の値を読み取り専用プロパティとして公開
member __.FirstName = firstName
member __.LastName = lastName
member __.Age = age
// DemoClassのインスタンスを生成する
let value = DemoClass("Taro", "Hoge", 25)
// クラスのインスタンスをマッチングする
// "{ ... }"によるパターンの記法については後述
match value with
// エラー FS1129: 型 'DemoClass' にフィールド 'FirstName' が含まれません
| { FirstName=fn; LastName=ln; Age=a } ->
System.Console.WriteLine(
"Name={0} {1}, Age={2}",
fn, ln, a)
# クラス型は上記のように定義しますが、詳細は省きます。コンストラクタが定義されること、プロパティが定義されてコンストラクタの引数が渡されることに注目してください。
しかし、マッチ式でプロパティが認識できません。これまで見てきたように、マッチ式は分解を行うこともできますが、単に値が一致することを確認する場合もあります。その両方の構文を満足させるためには、上記のような構文をサポートするだけでは不十分で、DemoClassのインスタンスを任意の値で初期化できて、かつ副作用が存在しない事が明らかであるような、宣言的な手段が必要です。
F#には「レコード型」という種類の型があり、この目的に使うことができます。レコード型の中身(IL)は単なるクラス型ですが、F#上では定義の構文が異なります。
// レコード型を定義する
type DemoRecord = {
FirstName : string
LastName : string
Age : int
}
// レコード型の初期化(型は推測される)
let value = {
FirstName = "Taro"
LastName = "Hoge"
Age = 25
}
// レコード型をマッチングする
match value with
| { FirstName=fn; LastName=ln; Age=a } ->
// レコード型の各フィールドを分解できる
System.Console.WriteLine(
"Name={0} {1}, Age={2}",
fn, ln, a)
レコード型を使うと、判別共用体と同じく型推論が働くようになります。上記の例ではvalueのインスタンスを生成するのに、型名”DemoRecord”を書く必要がありません。レコード型はF#によってフィールド(FirstName・LastName・Age)の初期化方法が管理可能な形で定義されるので、式だけで初期化が可能になり、パターンマッチングでもフィールド名を自動的に特定して値の分解もできるようになるのです。
レコード型のパターンには、”{ … }”という記法を使う事が出来ます。コード例に示した通り、「フィールド名=(マッチ式)」として記述できます。右辺を「マッチ式」と書いた通り、ここに更にネストしたマッチ式を書くことが出来ます。
さらなる例は省きますが、このように、レコード型もこれまでのマッチ式と柔軟に組み合わせて使用することができます。
真打ち・アクティブパターン
ここまでのパターンマッチングでも、もうかなりの応用力がある武器ですが、今まではささやかな序章だった・真のラスボスは云々、と言ったら信じてもらえますか? (*´Д`)
私が真面目にF#をモノにしようと決めたきっかけが、このアクティブパターンです。アクティブパターンは大きく2種類あるのですが、順番に解説します。どちらのアクティブパターンも「パターンマッチングを動的に実行できる」事が特徴です。
動的な分解
前節で、クラス型のメンバーを直接キャプチャ出来ないため、レコード型を使用するという例を示しました。アクティブパターンを使用すれば、クラスのメンバーを直接分解・キャプチャさせることが出来ます。
// DateTimeを分解してタプルで返すレコグナイザー関数
let (|Date|) (dt: System.DateTime) =
dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.DayOfWeek
let now = System.DateTime.Now
// DateTimeをマッチングする
match now with
// レコグナイザーを使ってタプルに分解する
| Date (yyyy, MM, dd, HH, mm, _, _) ->
System.Console.WriteLine(
System.String.Format(
"yyyy:{0} MM:{1} dd:{2} HH:{3} mm:{4}",
yyyy, MM, dd, HH, mm))
# 本来DateTimeはクラスではなくて構造体ですが、違いはありません。
マッチングを動的に判定するには、「アクティブレコグナイザー関数」を定義します。この関数の名前は、何かしら魔術めいた括弧 “(| … |)” で括られていますが、これは「バナナクリップパターン」(バナナのように見えますね?)と呼びます。
中身は単なる関数です。したがってどのような値でも返すことが出来、返された値がマッチ式でキャプチャ出来るようになります。ここでは、分解した結果をタプルで返しているので、マッチ式でもタプルで受け取っています。そして、キャプチャしたくない要素はアンダースコアで捨てることもできます。
値を解析して分類分けできる
与えられた値に応じた結果を動的に変えられると便利です。例えば、判別共用体はTagによってマッチの有無が(静的に)判定されますが、この判定を動的に実行できるようなものです。
// ゼロ・奇数・偶数を識別するレコグナイザー関数
let (|Zero|Even|Odd|) value =
match value, (value % 2) with
| 0, _ -> Zero
| _, 0 -> Even (value / 2) // 除算結果を返す
| _, 1 -> Odd (value / 2) // 除算結果を返す
let value = 131
match value with
// レコグナイザーを使って判定とキャプチャを行う
// ゼロにマッチ
| Zero ->
System.Console.WriteLine("Zero")
// 偶数にマッチ
| Even half ->
System.Console.WriteLine(
System.String.Format(
"Even: HalfValue={0}", half))
// 奇数にマッチ
| Odd half ->
System.Console.WriteLine(
System.String.Format(
"Odd: HalfValue={0}", half))
// (Zero・Even・Odd以外の値にはなり得ない)
レコグナイザー関数の名前は、更に奇妙な事になっています。この例では、レコグナイザーの結果として取りうる値を “|” で区切ります。値がゼロ・偶数・奇数に応じて、”Zero”, “Even” 又は “Odd” の値を返すことを意味します。また、EvenやOddの場合は、除算の結果(value / 2)も返しています。
このレコグナイザーをマッチ式で使うと、マッチングの実行時に関数が呼び出され、値を動的に判定してZero・Even・Oddを返すので、それぞれのマッチ式にマッチします。Zeroの場合は値が得られませんが、EvenとOddは除算結果が返されるので、値をキャプチャすることが出来ます(half)。
また、このレコグナイザーを使うと言う事は、値がZero・Even・Oddの何れかにしか分類されないことがはっきりしているため、それ以外の値をとる事がありません(その他にマッチする式が不要)。
このマッチ式を見ていると、判別共用体を動的に判定しているかのように見えますね。最初のswitch-caseの説明で、数値や文字列だけではなく、任意の値(インスタンス)に対してEqualsやIEquatableを使ってカスタムコードで判定を行うことができれば良いのに、という話をしました。レコグナイザー関数を使えば、そういった判定を安全に行うことが出来ます。
値を解析してマッチ式の判定に影響させる
上記のレコグナイザーは、取りうる結果の種類があらかじめ完全に把握されている(Zero・Even・Oddの何れかしかない)場合に使えます。しかし、判断が該当しない場合は、単に「該当なし」として判定したい事もあります。
// 正の偶数を識別するレコグナイザー関数
let (|IsPositiveEven|_|) value =
if value < 0 then
None // 該当しない
else if (value % 2) = 1 then
None // 該当しない
else
Some (value / 2) // 該当したので除算した結果を返す
let value3 = 131
match value3 with
// レコグナイザーを使って判定とキャプチャを行う
| IsPositiveEven half ->
System.Console.WriteLine(
System.String.Format(
"Plus & even: HalfValue={0}", half))
// 正の偶数ではなかった
| v1 ->
System.Console.WriteLine(
System.String.Format(
"Other: {0}", v1))
このレコグナイザー関数は、末尾にアンダースコアを付けることで「パーシャルレコグナイザー」として機能するようになります。パーシャルレコグナイザーは、戻り値が “Option型” の値で返される事を想定します。
Option型は判別共用体で、”Some”と”None”のTagを持ちます。C#で言うところの”Nullable”に近いのですが、存在するならその値、または値が存在しないと言う事を表し、判別共用体なので安全に使用できます。上記の例では、与えられたvalueが負数か0なら”None”、それ以外の場合はその値の1/2を返すようになっています。
このレコグナイザーを使用すると、マッチ式の評価は “Some” の場合にだけ変換された値がキャプチャされ、”None” の場合はそのマッチ式がマッチしなかったことになり、他のマッチ式を試行します。従って、この場合は変換に失敗したマッチを用意する必要があります(マッチが漏れていると警告が発生します)。
アクティブレコグナイザーを使うことで、これまで静的な宣言による判定しかできなかったコードで、動的に複雑な判定を行わせることが出来るようになります。しかもその実装は単なる関数であり、判定の詳細を関数内に完全にカプセル化することが出来ます。こうして設計されたレコグナイザーは、他のパターンマッチングのすべてのパターンと直交的に組み合わせて使用できるため、非常に高い表現力・安全性・応用性を持ちます。
まとめ
F#でのパターンマッチングを、これまでの一般的な比較・分岐処理と対比させて解説しました。私の感想としては、やはりアクティブパターン強し!と言う所です。私はC#のLINQも好きですが、LINQが凄かったのは、IEnumerable<T>による統一された集合処理と、拡張メソッドによる直交性のある独自の拡張が可能であったことだと思います。F#で直接対比するとすればシーケンス処理がそれに該当するのですが、正直すでにLINQが当たり前なので、F#で同じことが出来ることはある意味当然というイメージでした(むしろ、F#にはシーケンス処理を汎化した「コンピュテーション式」があり、そっちはこれまた大海原です)。
パターンマッチングの良いところは、今までであれば「デザインパターン」に落とし込んで回避しなければならなかったような複雑な構造を、シンプルに、簡単に、わかりやすく、安全に記述出来て、検証可能で、しかもすぐに応用することが出来るようになった事だと思います。例えば、前半で示したパターンの組み合わせについて、抽象クラスを導入して実装を派生させて…という考え方ももちろん可能です。しかし、F#の世界ではそんな事をしなくても、タプルや判別共用体とパターンマッチングを組み合わせるだけで解決できてしまいます。
プログラミング言語の進化としては、ジェネリックプログラミング以来のインパクトを感じました。F#ではこれらがもう当たり前の世界になっていて、C#もこれからパターンマッチングを取り込んでいくと思います。応用性の高い、直交性のある拡張を期待したいです。
補足: パターンマッチングのマッチ式内で使用できるパターンは、すべて網羅していません。より複雑なパターンもサポートされています。この記事で解説した内容を把握していれば、それらについても容易に理解できると思います。詳しくはMSDNを参照してください。
修正履歴
- タプルの型Tuple<…>が隠され、Item1, Item2等のプロパティに直接アクセスできないので修正しました。もちろん、リフレクションを使用した場合はこの限りではありません。
- F#のリストは順方向リンクリストであることを明示しました。内部的にはFSharpList<‘T>というクラスです。
- “:?”や”[|…|]”や”(|…|)”は演算子ではないので、記述を改めました。MSDNには「パターン」と書かれています。パターンという語は一般的過ぎて説明には適さないと思っているのですが、合わせるようにしています(まだ文書全体では統一されていません)。
- “:?”パターンでマッチさせたい目的が、型の動的判断ぐらいしかないという記述は無くしました。実際にはすべてのパターンは直交的に組み合わせ可能なので、実現したい判断次第ですね。
- 関数名をよりふさわしく修正し、int.TryParseという使われていない解説を修正しました。
- OptionとNullableの関係について修正しました。Optionは判別共用体である事にフォーカスさせました。
※まだ修正作業中です。気になる方はfsugjpでの指摘を参照してください。