パターンマッチング
次に、パターンマッチング(match式)について解説します。パターンマッチングはC#のSwitchのようなものです。式をそれぞれのパターンと比較し、マッチした際にその右辺にある式を実行します。パターンマッチングの各パターンはマッチングしたい値の構造を定義する小言語とも言えます。
--------------------------------------------------------------------- match式 with | パターン [ when条件] -> 結果式 | | ... ---------------------------------------------------------------------
フィボナッチ関数を用いながら、C#のSwitchとF#のパターンマッチングを比較してみましょう。
Public static int testFib(int n) { switch(n) { case 0: return 0; case 1: return 1; default: return testFib(n - 1) + testFib(n - 2); } }
f(n)=f(n-1)+f(n-2)で成り立つフィボナッチ数の例はいたってシンプルでご存知だと思うので、上記のスクリプトの説明は省きます。次にF#のパターンマッチングを用いた例を見てください。
let rec testFib n = let result = match n with | 0 -> 0 | 1 -> 1 | _ -> testFib(n - 1) + testFib(n - 2) //※1 result
※1の部分のアンダーバーはC#のdefaultのようなもので、その他全てをパターンをキャッチします。このケースを一番上に持っていくとどのケースもマッチしなくなってしまうので、必ず最後に定義してください。
一見スクリプトがシンプルですっきりしている、くらいに見えるかもしれませんが、実はここには大きな違いがあります。例えば、上記のF#のパターンマッチングは、関数のように引数を受け取り値を返すことができます。同じ操作をC#でするなら、上記のコードはさらに煩雑になります。
さらに、上記のパターンマッチングは下記のように簡単に制約をつけることも可能です。if文でパターンマッチングに入る前に条件をつけず、パターンの一環として制約をつけることができます。
let rec testFib n = match n with | n when n < 0 -> failwith "値は0以上である必要があります。" //※2 | 0 -> 0 | 1 -> 1 | _ -> testFib (n - 1) + testFib (n - 2) testFib -2;;
▼
System.Exception: 値は0以上である必要があります。 at FSI_0021.testFib(Int32 n) at <StartupCode$FSI_0022>.$FSI_0022.main@() Stopped due to error
nが0未満の場合には事前に警告を表示させます(※2)。それから、他のケースとのマッチングが始まります。
2つ以上のパターンに同じ値を関連づけることも可能です。
match n with | 0 | 1 -> 0 ※3 | n -> n * (n-1)
※3はnが0もしくは1にマッチする場合には、0をバインドします。
判別共用体(Discriminated Union)
次にF# LOPの抽象的表現のもう1つの代表的機能、判別共用体(Discriminated Union)について解説します。判別共用体もまたその機能からそれ自体が小言語(宣言型言語)と言えます。
----------------------------------------------- type 判別共用体名 = | 名前付きケース [of 型 [ * 型 ...] | 名前付きケース [of型 [ * 型 ...] ... -----------------------------------------------
判別共用体はC#などの列挙型(Enumeration)に似ています。列挙型同様に値は固定のものではありません。違っているのは、それぞれの選択肢に名前付きケースが与えられていて、それに値を関連付けることができるところです。ある判別共用体の型の値を作成する際に、その初期化用コンストラクタとなり得る選択肢ひとつずつに名前を付けたものを名前付きケースと呼びます。名前付きケースは大文字で始まる必要があります。名前付きケースはそれぞれ、値がある場合や無い場合、また値の型が異なることもあります。つまり、有効ケースとエラーケースなど特殊なケースを持つ可能性があるデータ、インスタンスごとに型の異なるデータなどにも使用できます。
例を挙げてみましょう。例えば、下記のように異なるデータ型の値を定義する際に、C#では親クラスShapeを派生させてそれぞれの型を1つずつ定義する必要があります。
class TestShape { public class TestCircle : TestShape { public float Value; } public class TestSquare : TestShape { public double Value; } public class TestRectangle : TestShape { public System.Drawing.Point Value; } }
しかし、F#の判別共用体を使用すれば、Shape型にそれぞれが名前付きケースとして定義でき、個々のクラスを定義する必要がありません(正確には、F#がここの名前付きケースに共用体にShapeから派生したインナークラスを作成するためです)。
type TestShape = | TestCircle of float | TestSquare of double | TestRectangle of System.Drawing.Point;; //値の作成 let TestShape1 = TestCircle(10.5);;
▼
type TestShape = | TestCircle of float | TestSquare of double | TestRectangle of System.Drawing.Point > val TestShape1 : TestShape = TestCircle 10.5
判別共用体もパターンマッチングと共に使用することで、複雑なデータ構造や複数のデータ型にまたがるような操作をより簡潔に表現できるようになり、優れた効果を発揮します。
判別共用体とパターンマッチングを一緒に利用するメリットの一部を例を用いて説明します。
type TestPlayer = //判別共用体の定義※1 |Junior of int |Senior of int;; let testCategoryPrint (player : TestPlayer) = //パターンマッチングでそれぞれに操作を定義※2 match player with | Junior(x) -> printfn "Junior Age %d" x | Senior(y) -> printfn "Senior Age %d" y;; type TestPlayer = //名前付きケースを追加する※3 |Junior of int |Senior of int |Child of double;; let testCategoryPrint (player : TestPlayer) = //新たに追加されたケースへの対応が漏れている※4 match player with | Junior(x) -> printfn "Junior Age %d" x | Senior(y) -> printfn "Senior Age %d" y;;
▼
type TestPlayer =
| Junior of int
| Senior of int
>
val testCategoryPrint : TestPlayer -> unit
>
type TestPlayer =
| Junior of int
| Senior of int
| Child of double
>
match player with
----------^
^
stdin(16,11): warning FS0025: Incomplete pattern matches on this expression. For example, the value 'Child (_)' may indicate a case not covered by the pattern(s).
val testCategoryPrint : TestPlayer -> unit
val testCategoryPrint : TestPlayer -> unit
上の例では、まずtestPlayerという判別共用体があります。名前付きケースとして、int型の値(年齢)を持つJuniorと同じくInt型の値(年齢)を持つSeniorが定義されています(※1)。これをパターンマッチングでケースごとに異なる内容を表示させるように定義します(※2)。次に、この共用体に新しいケースとしてChildを追加したくなったとします(※3)(Childは1歳未満の子供もいるため、型はdoubleにします)。このことによって、先ほどのパターンマッチングでは、新しいChildのケースに関してカバーされていないため、コンパイラは警告を与えてくれます(※4)。C#の列挙型でも同様の概念を表し、Switchでパターン時の操作を定義することはもちろん可能ですが、コンパイラによるエラーチェックは比較的緩く、また、パターンをネストすることができません。定義された名前付きケースとそれに関連付けられた値のセットから1セットだけを排他的に持てるのも判別共用体の場合だけです。
まとめ
今回解説した抽象的表現のテクニックを用いることにより、オブジェクト指向では表現しにくい問題も、F#コードでDSLのような小言語を作成し、それを用いて解決できます。次回は、残りの具体的表現について解説します。