M.Hiroi's Home Page

お気楽 Scala プログラミング入門

パターンマッチング


Copyright (C) 2014-2024 Makoto Hiroi
All rights reserved.

はじめに

Scala は「パターンマッチング (pattern matching)」を使って条件分岐を行うことができます。また、リストもパターンマッチングで分解することができます。これらの機能は論理型言語 Prolog のパターンマッチングと大変よく似ています。パターンマッチングは Scala だけではありません。Erlang, SML/NJ, OCaml, Haskell など関数型言語の特徴のひとつで、とても便利で強力な機能です。

●val や var でのパターンマッチング

Scala の場合、val や var で変数を宣言するとき、パターンマッチングを使うことができます。次の例を見てください。

scala> val (a, b) = (1, 2)
val a: Int = 1
val b: Int = 2

scala> val (c, d) = ((1, 2), 3)
val c: (Int, Int) = (1,2)
val d: Int = 3

scala> val ((e, f), g) = ((1, 2), 3)
val e: Int = 1
val f: Int = 2
val g: Int = 3

右辺 (a, b) がパターンを表します。要素が 2 つ並んでいるので、2 要素のタプルを表すパターンになります。パターン (a, b) と左辺の (1, 2) を照合して、変数部分に対応する要素を取り出して変数にセットします。次の例のように、(c, d) と ((1, 2), 3) を照合すると、c は (1, 2) になり、d は 3 になります。

パターンは入れ子にしてもかまいません。((e, f), g) と ((1, 2), 3) を照合すると、e = 1, f = 2, g = 3 となります。このように、パターンを使ってタプルの要素を取り出すことができます。ただし、型が違うと照合に失敗してエラーになるので注意してください。

リストもパターンマッチングを使って要素を取り出すことができます。リストのパターンはコンス演算子 :: を使って表します。次の例を見てください。

scala> val x::xs = List(1, 2, 3, 4, 5)
-- Warning: ---------------------------------
  ... 省略 ...
val x: Int = 1
val xs: List[Int] = List(2, 3, 4, 5)

scala> val y::ys = List(1)
-- Warning: ---------------------------------
  ... 省略 ...
val y: Int = 1
val ys: List[Int] = List()

scala> val z::zs = ys
-- Warning: ---------------------------------
  ... 省略 ...
scala.MatchError: List() (of class scala.collection.immutable.Nil$)
  ... 省略 ...

Scala3 では Warning が表示されますが、ここでは無視してください。x::xs がパターンを表します。このパターンはリストとマッチングして、先頭の要素が x に、先頭要素を取り除いた残りのリストが xs に束縛されます。リストの要素が 1 つしかない場合、y::ys は y が 1 とマッチングして、ys は空リストとマッチングします。また、z::zs は空リスト ys とはマッチングしません。マッチングエラーが送出されます。このように、関数 head や tail を使わなくてもリストを分解することができます。

●match 式

match 式を使うと、パターンマッチングで条件分岐を行うことができます。match 式の構文を示します。

式0 match {
    case pattern_1 => 式1
    case pattern_2 => 式2
      ...
    case pattern_n => 式n
}

case で区切られた部分をマッチング節 (matching clause) といいます。match 式は式 0 の評価結果とパターンを照合し、マッチングする節を選択して実行します。たとえば、式 0 の結果と pattern_1 がマッチングした場合、式 1 を評価してその結果が match 式の返り値になります。マッチングしない場合は次のパターンを調べます。マッチングするパターンが見つからない場合はエラーになります。

なお、一度マッチング節が選択された場合、それ以降の節は選択されません。また、式 1 から式 n の結果は同じ型でなければいけません。ご注意ください。

簡単な例を示しましょう。match 式を使って階乗を求める関数 fact を定義すると次のようになります。

scala> def fact(n: Int): BigInt = n match {
     | case 0 => 1
     | case m => m * fact(m - 1)
     | }
def fact(n: Int): BigInt

scala> fact(10)
val res0: BigInt = 3628800

scala> fact(20)
val res1: BigInt = 2432902008176640000

パターンが定数の場合、同じ値とマッチングします。最初の定義はパターンが 0 なので、引数 n が 0 の場合にマッチングします。パターンが変数の場合はどんな値とでもマッチングします。そして、変数はその値に初期化されます。したがって、n が 0 以外の場合は 2 番目のパターンと一致して、変数 m の値は n になります。ここで再帰呼び出しが行われます。なお、変数の有効範囲はそのマッチング節の中だけになります。

変数 m は n と同じ値なので、パターンにワイルドカード ( _ ) を使って次のように定義してもかまいません。

case _ => n * fact(n - 1)

このように、if を使わなくてもパターンマッチングでプログラムを作ることができます。

パターンマッチングを使うときは、マッチング節を定義する順番に気をつけてください。fact の場合、最初にパターン m や _ を定義すると、引数が 0 の場合でもマッチングするので、パターン 0 のマッチング節が実行されることはありません。特定のパターンから定義するように注意してください。

●リストのパターンマッチング

次はリストのパターンマッチングについて、もう少し詳しく説明しましょう。たとえば、パターンを使って関数 append を定義すると次のようになります。

scala> def append[A](xs: List[A], ys: List[A]): List[A] = xs match {
     | case Nil => ys
     | case z::zs => z :: append(zs, ys)
     | }
def append[A](xs: List[A], ys: List[A]): List[A]

scala> append(List(1,2,3), List(4,5,6))
val res2: List[Int] = List(1, 2, 3, 4, 5, 6)

scala> append(Nil, List(1,2,3))
val res3: List[Int] = List(1, 2, 3)

最初のパターンは、xs が空リストのときにマッチングします。2 番目のパターンはリスト xs を先頭要素 z と残りのリスト zs に分解します。このように、match 式を使うとリストを簡単に分解することができます。

リストを表すパターンは x::xs だけではありません。よく使われるパターンを次に示します。

(1) x::Nil      要素が 1 つのリストとマッチング
(2) x::y::Nil   要素が 2 つのリストとマッチング
(3) x::xs       要素が 1 つ以上あるリストとマッチング
(4) x1::x2::xs  要素が 2 つ以上あるリストとマッチング
(5) x1::x1::xs  エラー

(5) のように、パターンの中に同名の変数を使うことはできません。この場合、x1::x2::xs とマッチングさせてから x1 と x2 が等しいかチェックします。このような場合、パターンの後ろで if を使うと便利です。if の構文を示します。

case パターン if (条件式) => 式

このようなマッチング節を「ガード付き節」といいます。パターンとの照合に成功して、かつ if の条件式が真を返す場合に限り式が評価されます。たとえば、if を使って (5) を実現すると次のようになります。

case x1::x2::xs if (x1 == x2) => 式1
case x1::x2::xs if (x1 < x2) => 式2
case x1::x2::xs => 式3

パターンとの照合に成功して、x1 == x2 の場合は式 1 が評価されます。x1 < x2 の場合は式 2 が評価され、それ以外の場合は式 3 が評価されます。

また、もっと複雑なリストもパターンで表すことができます。

(1) (x, y)::xs  要素がタプルのリストとマッチング
    ex) List((1, 2), (3, 4), (5, 6)) => x = 1, y = 2, xs = List((3, 4), (5, 6))

(2) (x::xs)::ys 要素がリストのリスト ('a list list) とマッチング
    ex) List(List(1, 2, 3), List(4, 5), List(6)) => x = 1, xs = List(2, 3), ys = List(List(4, 5), List(6))

ご参考までに、関数 length, reverse, member をパターンマッチングを使って書き直してみましょう。

リスト : パターンマッチングの使用例

object sample0402 {
  def length[A](xs: List[A]): Int =
    xs match {
      case Nil => 0
      case _::ys => 1 + length(ys)
    }

  def reverse[A](xs: List[A]): List[A] =
    xs match {
      case Nil => Nil
      case y::ys => reverse(ys) ::: List(y)
    }

  def member[A](x: A, xs: List[A]): Option[A] =
    xs match {
      case Nil => None
      case y::ys if (x == y) => Some(y)
      case y::ys => member(x, ys)
    }
}
scala> :load sample0402.scala
// defined object sample0402

scala> import sample0402._

scala> length(List(1,2,3,4,5))
val res4: Int = 5

scala> reverse(List(1,2,3,4,5))
val res5: List[Int] = List(5, 4, 3, 2, 1)

scala> member(3, List(1,2,3,4,5))
val res6: Option[Int] = Some(3)

●Option のパターンマッチング

Option もパターンマッチングすることができます。次の例を見てください。

scala> def foo(a: Option[String]): String = a match {
     | case None => "oops!"
     | case Some(mes) => mes
     | }
def foo(a: Option[String]): String

scala> foo(None)
val res7: String = oops!

scala> foo(Some("hello"))
val res8: String = hello

パターンマッチングで Some と None を区別するだけではなく、Some(mes) のように変数 mes を指定すると、Some の値を mes に取り出すことができます。

●連想リスト

それでは簡単な例題として、「連想リスト (association list : a-list)」を作ってみましょう。連想リストは Lisp でよく用いられるデータ構造で、Scala ではキーとデータのタプルを要素とするリストで実現できます。データ型で記述すると List[(A, B)] になり、A がキーで B がデータに対応するデータになります。次の図を見てください。


                     ┌────┬────┬────┬──→ データ 
                     │        │        │        │
 連想リスト => [("a", 1), ("b", 2), ("c", 3), ("d", 4)]
                 │        │        │        │
                 └────┴────┴────┴──→ キー

                        図 : 連想リストの構造

上図の場合、文字列 "a", "b", "c", "d" がキーで、整数 1, 2, 3, 4 がデータとなります。一般に、キーには文字列を用いることが多く、データは何でもかまいません。

それでは連想リストを操作する関数を作りましょう。関数名は Common Lisp から拝借しました。次のリストを見てください。

リスト : 連想リスト

object sample0403 {

  def acons[A, B](x: A, y: B, xs: List[(A, B)]): List[(A, B)] = (x, y) :: xs

  def pairlis[A, B](xs: List[A], ys: List[B]): List[(A, B)] =
    (xs, ys) match {
      case (Nil, _) | (_, Nil) => Nil
      case (x::xs1, y::ys1) => (x, y)::pairlis(xs1, ys1)
    }

  def assoc[A, B](key: A, xs: List[(A, B)]): Option[B] =
    xs match {
      case Nil => None
      case (y, v)::ys if (y == key) => Some(v)
      case _::ys => assoc(key, ys)
    }
}

関数 acons はキー x とデータ y と連想リスト xs を受け取り、xs の先頭に x と y を追加します。これは簡単ですね。関数 pairlis は 2 つのリストを受け取り、第 1 引数のリストの要素がキー、第 2 引数のリストの要素がデータとなる連想リストを生成します。引数のリストの長さが違う場合は短い方に合わせます。

pairlis の match が照合する式の値はタプルになるので、パターンにはタプルを使います。最初の節の | は OR の意味で、左右どちらかのパターンがマッチすれば、その節が選択されます。つまり、xs または ys のどちらかが空リストであれば Nil を返します。

関数 assoc は指定したキーと等しいデータを探します。見つけた場合、値を Some に格納して返します。見つからない場合は None を返します。このように、パターンマッチングとガード節を使うと簡単にプログラムを作ることができます。

それでは、簡単な実行例を示します。

scala> :load sample0403.scala
// defined object sample0403

scala> import sample0403._

scala> val alist = pairlis(List("foo", "bar", "baz"), List(1,2,3))
val alist: List[(String, Int)] = List((foo,1), (bar,2), (baz,3))

scala> assoc("foo", alist)
val res9: Option[Int] = Some(1)

scala> assoc("bar", alist)
val res10: Option[Int] = Some(2)

scala> assoc("baz", alist)
val res11: Option[Int] = Some(3)

scala> assoc("oops", alist)
val res12: Option[Int] = None

scala> val alist1 = acons("oops", 4, alist)
val alist1: List[(String, Int)] = List((oops,4), (foo,1), (bar,2), (baz,3))

scala> assoc("oops", alist1)
val res13: Option[Int] = Some(4)

●挿入ソート

もう一つ簡単な例題としてリストをソートする関数を作ってみましょう。ソート (sort) は、ある規則に従ってデータを順番に並べ換える操作のことです。たとえば、データが整数であれば大きい順に並べる、もしくは小さい順に並べます。Scala のライブラリにはリストをソートする関数がありますが、私達でも簡単に作ることができます。今回は「挿入ソート (insert sort)」を取り上げます。

挿入ソートの考え方はとても簡単です。ソート済みのリストに新しいデータを挿入していくことでソートを行います。たとえば、List(2, 4, 6) に 5 を挿入する場合、リストの要素 n と 5 を順番に比較して、5 < n を満たす位置に 5 を挿入すればいいわけです。この場合は、4 と 6 の間に 5 を挿入すればいいですね。

ソートするリストは tail で分解していくと空リストになります。これをソート済みのリストと考えて、ここにデータを挿入していきます。プログラムは次のようになります。

リスト : 挿入ソート

object sample0404 {
  def insertSort(xs: List[Int]): List[Int] = {
    def insertElement(x: Int, xs: List[Int]): List[Int] =
      xs match {
        case Nil => List(x)
        case y::ys => if (x < y) x::y::ys else y::insertElement(x, ys)
      }
    xs match {
      case Nil => Nil
      case y::ys => insertElement(y, insertSort(ys))
    }
  }
}

関数 insertSort は引数のリスト xs を再帰呼び出しで分解します。insertSort を再帰呼び出ししてリスト ys をソートし、そのリストに先頭要素 y を関数 insertElement で挿入します。

局所関数 insertElement は再帰呼び出しでリストをたどり、データ x を挿入する位置を探します。引数のリストが空リストであれば、x が一番大きなデータです。List(x) を返します。これで、リストの最後尾に x を挿入することができます。次の節でリストを分解して、x が先頭の要素 y よりも小さい場合は、その位置に x を挿入します。そうでなければ、insertElement を再帰呼び出しします。

それでは実行してみましょう。

scala> :load sample0404.scala
// defined object sample0404

scala> import sample0404._

scala> insertSort(List(5,6,4,7,3,8,2,9,1,0))
val res14: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

scala> insertSort(List(9,8,7,6,5,4,3,2,1,0))
val res15: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

scala>  insertSort(List(0,1,2,3,4,5,6,7,8,9))
val res16: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

挿入ソートはデータ数が多くなると実行時間がかかります。データ数を N とすると、実行時間は N の 2 乗に比例します。挿入ソートは簡単ですが遅いアルゴリズムなのです。

なお、次のように比較関数 (引数 lt) を渡すと多相型関数として定義することができます。

リスト : 挿入ソート (多相型)

  def insertSort[A](lt: (A, A) => Boolean, xs: List[A]): List[A] = {
    def insertElement(x: A, xs: List[A]): List[A] =
      xs match {
        case Nil => List(x)
        case y::ys => if (lt(x, y)) x::y::ys else y::insertElement(x, ys)
      }
    xs match {
      case Nil => Nil
      case y::ys => insertElement(y, insertSort(lt, ys))
    }
  }

簡単な実行例を示します。

scala> insertSort((x:Int,y:Int) => x < y, List(5, 4, 3, 2, 1))
val res0: List[Int] = List(1, 2, 3, 4, 5)

scala> insertSort((x:Double,y:Double) => x < y, List(5.5, 4.4, 3.3, 2.2, 1.1))
val res1: List[Double] = List(1.1, 2.2, 3.3, 4.4, 5.5)

初版 2014 年 7 月 27 日
改訂 2024 年 12 月 17 日