OCaml は「パターンマッチング (pattern matching)」を使って条件分岐を行うことができます。また、リストもパターンマッチングで分解することができます。これらの機能は論理型言語 Prolog のパターンマッチングと大変よく似ています。パターンマッチングは ML の特徴の一つで、とても便利な機能です。
パターンマッチングは match 式で行います。match 式の構文を示します。
match 式0 with pattern_1 -> 式1 | pattern_2 -> 式2 ... | pattern_n -> 式n
| で区切られた部分をマッチング節 (matching clause) といいます。match 式は式 0 の評価結果とパターンを照合し、マッチングする節を選択して実行します。たとえば、式 0 の結果と pattern_1 がマッチングした場合、式 1 を評価してその結果が match 式の返り値になります。マッチングしない場合は次のパターンを調べます。マッチングするパターンが見つからない場合はエラーになります。
なお、一度マッチング節が選択された場合、それ以降の節は選択されません。また、式 1 から式 n の結果は同じ型でなければいけません。ご注意ください。
簡単な例を示しましょう。match 式を使って階乗を求める関数 fact を定義すると次のようになります。
リスト 1 : 階乗 (* パターンマッチング未使用 *) let rec fact n = if n = 0 then 1 else n * fact (n - 1) (* パターンマッチング *) let rec fact n = match n with 0 -> 1 | n' -> n' * fact (n' - 1)
パターンが定数の場合、同じ値の引数とマッチングします。最初の定義はパターンが 0 なので、引数が 0 の場合にマッチングします。これは if n = 0 then 1 と同じ処理です。パターンが変数の場合はどんな値とでもマッチングします。そして、変数はその値に束縛されます。したがって、n が 0 以外の場合は 2 番目のパターンと一致し、変数 n' の値は n になります。ここで再帰呼び出しが行われます。
変数 n' は n と同じ値なので、パターンにワイルドカードを使って次のように定義してもかまいません。
| _ -> n * fact (n - 1)
このように、if を使わなくてもパターンマッチングでプログラムを作ることができます。
パターンマッチングを使うときは、マッチング節を定義する順番に気をつけてください。fact の場合、最初にパターン n' を定義すると、引数が 0 の場合でもマッチングするので、パターン 0 のマッチング節が実行されることはありません。特定のパターンから定義するように注意してください。
match 式によるパターンマッチングは function 文を使うと簡単になります。function 文は匿名関数と match 式を組み合わせたものです。
function fun 引数 -> match 引数 with pattern_1 -> 式1 pattern_1 -> 式1 | pattern_2 -> 式2 | pattern_2 -> 式2 ... ... | pattern_n -> 式n | pattern_n -> 式n
関数 fact を function 文で書き直すと次のようになります。
リスト 2 : 階乗 (2) let rec fact = function 0 -> 1 | n -> n * fact (n - 1)
カリー化関数で function 文を使うとき、一番最後の引数がマッチングの対象になることに注意してください。たとえば、2 つの整数の最大公約数を求める関数 gcd は次のようになります。
リスト 3 : 最大公約数 let rec gcd a = function 0 -> a | b -> gcd b (a mod b)
リストの操作は関数 hd, tl でリストを分解するよりも「パターンマッチング」を使った方が簡単です。リストもパターンとマッチングすることができます。リストのパターンはコンス演算子 :: を使って表します。たとえば、パターンを使って関数 append を定義すると次のようになります。
リスト 4 : リストの連結 let rec append xs ys = match xs with [] -> ys | x :: xs -> x :: (append xs ys)
最初のパターンは、xs が [ ] とマッチングします。次の x :: xs がパターンを表します。このパターンはリストとマッチングして、先頭の要素が x に、先頭要素を取り除いた残りのリストが xs に束縛されます。このように、関数 hd や tl を使わなくてもリストを分解することができます。
リストを表すパターンは x::xs だけではありません。よく使われるパターンを次に示します。
(1) [x] 要素が 1 つのリストとマッチング (2) [x; y] 要素が 2 つのリストとマッチング (3) x::xs 要素が 1 つ以上あるリストとマッチング (4) x1::x2::xs 要素が 2 つ以上あるリストとマッチング (5) x1::x1::xs エラー
(5) のように、パターンの中に同名の変数を使うことはできません。この場合、x1::x2::xs とマッチングさせてから x1 と x2 が等しいかチェックします。このような場合、キーワード when を使うと便利です。when の構文を示します。
パターン when 条件式 -> 式
このようなマッチング節を「ガード付き節」といいます。パターンとの照合に成功して、かつ when の条件式が真を返す場合に限り式が評価されます。たとえば、when を使って (5) を実現すると次のようになります。
| x1::x2::xs when x1 = x2 -> 式1 | x1::x2::xs when x1 < x2 -> 式2 | x1::x2::xs -> 式3
パターンとの照合に成功して、x1 = x2 の場合は式 1 が評価されます。x1 < x2 の場合は式 2 が評価され、それ以外の場合は式 3 が評価されます。
また、もっと複雑なリストもパターンで表すことができます。
(1) (x, y)::xs 要素が組のリストとマッチング ex) [(1, 2); (3, 4); (5, 6)] => x = 1, y = 2, xs = [(3, 4); (5, 6)] (2) (x::xs)::ys 要素がリストのリスト ('a list list) とマッチング ex) [[1; 2; 3], [4; 5], [6]] => x = 1, xs = [2; 3], ys = [[4; 5]; [6]]
ご参考までに、関数 length, reverse, member をパターンマッチングを使って書き直してみましょう。
リスト 5 : パターンマッチングの使用例 (* リストの長さ *) let rec length = function [] -> 0 | x :: xs -> 1 + length xs (* リストの反転 *) let rec reverse = function [] -> [] | x :: xs -> reverse xs @ [x] (* 探索 *) let rec member x = function [] -> [] | (y :: ys) as xs when x = y -> xs | (y :: ys) -> member x ys
ここで、関数 member を見てください。パターン y :: ys を使うとリストを分解することができますが、分解した値 y や ys だけではなく、元のリストの値を参照したいときがあります。このような場合、as を使うと変数とパターンを同時に設定することができます。
パターン as 変数名
たとえば、(y :: ys) as xs と [1; 2; 3] をマッチングさせると、次のようになります。
xs => [1; 2; 3] y => 1 ys => [2; 3]
このように、パターン y :: ys とマッチングした場合、変数 xs の値は分解する前の [1; 2; 3] になります。
次の関数をパターンマッチングを使って書き直してください。
リスト : パターンマッチングの使用例 let rec make_list x n = match n with 0 -> [] | n -> x :: make_list x (n - 1) let rec take xs n = match (n, xs) with (0, _) | (_, []) -> [] | (_, y::ys) -> y :: take ys (n - 1) let rec drop xs n = match (n, xs) with (0, _) -> xs | (_, []) -> [] | (_, _::ys) -> drop ys (n - 1) let rec zip xs ys = match (xs, ys) with ([], _) | (_, []) -> [] | (x::xs1, y::ys1) -> (x, y) :: zip xs1 ys1 let rec unzip = function [] -> ([], []) | (x, y)::zs -> let (xs, ys) = unzip zs in (x::xs, y::ys)