ケースクラスとパターンマッチング

パターンマッチングは、Scalaを初めとする関数型言語に一般的な機能です。CやJavaのswitch文に似ていますが、より強力な機能です。しかし、パターンマッチングの真価を発揮するには、標準ライブラリまたはユーザが定義したケースクラス(case class)によるデータ型の定義が必要になります。

簡単なケースクラスによるデータ型を定義してみます。

sealed abstract class DayOfWeek
case object Sunday extends DayOfWeek
case object Monday extends DayOfWeek
case object Tuesday extends DayOfWeek
case object Wednesday extends DayOfWeek
case object Thursday extends DayOfWeek
case object Friday extends DayOfWeek
case object Saturday extends DayOfWeek

これは、一週間の曜日を表すデータ型です。CやJavaのenumに似ていますね。実際、同じように使うことができます。たとえば、以下のようにDayOfWeek型の変数にSundayを代入することができます。

val x: DayOfWeek = Sunday

objectまたはその他のデータ型は、パターンマッチングのパターンを使うことができます。この例では、このDayOfWeek型を継承した各objectをパターンマッチングのパターンを使うことができます。パターンマッチングの構文は再度書くと、

match {
  case pat1 =>
  case pat2 =>
  ...
}

のようになります。DayOfWeekの場合、次のようにして使うことができます。

scala> x match {
     |   case Sunday => 1
     |   case Monday => 2
     |   case Tuesday => 3
     |   case Wednesday => 4
     |   case Thursday => 5
     |   case Friday => 6
     | }
<console>:20: warning: match may not be exhaustive.
It would fail on the following input: Saturday
       x match {
       ^
res0: Int = 1

これは、xがSundayなら1を、Mondayなら2を…返すパターンマッチです。ここで、パターンマッチで漏れがあった場合、コンパイラが警告してくれます。この警告は、sealed修飾子をスーパークラス/トレイトに付けることによって、その(直接の)サブクラス/トレイトは同じファイル内にしか定義できないという性質を利用して実現されています。この用途以外でsealedはめったに使われないので、ケースクラスのスーパークラス/トレイトにはsealedを付けるものだと覚えておけば良いでしょう。

これだけだと、CやJavaの列挙型とあまり変わらないように見えますが、それらと異なるのは各々のデータは独立してパラメータを持つことができることです。また、パターンマッチの際はそのデータ型の種類によって分岐するだけでなく、データを分解することができることが特徴です。

例として四則演算を表す構文木を考えてみます。各ノードExpを継承し(つまり、全てのノードは式である)、二項演算を表すノードはそれぞれの子としてlhs(左辺)、rhs(右辺)を持つこととします。葉ノードとして整数リテラル(Lit)も入れます。これはIntの値を取るものとします。また、二項演算の結果として小数が現れた場合は小数部を切り捨てることとします。これを表すデータ型をScalaで定義すると次のようになります。

sealed abstract class Exp
case class Add(lhs: Exp, rhs: Exp) extends Exp
case class Sub(lhs: Exp, rhs: Exp) extends Exp
case class Mul(lhs: Exp, rhs: Exp) extends Exp
case class Div(lhs: Exp, rhs: Exp) extends Exp
case class Lit(value: Int) extends Exp

全てのデータ型にcase修飾子がついているので、これらのデータ型はパターンマッチングのパターンとして使うことができます。この定義から、1 + ((2 * 3) / 2)という式を表すノードを構築します。

scala> val example = Add(Lit(1), Div(Mul(Lit(2), Lit(3)), Lit(2)))
example: Add = Add(Lit(1),Div(Mul(Lit(2),Lit(3)),Lit(2)))

このexampleノードを元に四則演算を定義する関数を定義してみます。関数の定義の詳細は後ほど説明しますが、ここでは雰囲気だけをつかんでください。

scala> def eval(exp: Exp): Int = exp match {
     |   case Add(l, r) => eval(l) + eval(r)
     |   case Sub(l, r) => eval(l) - eval(r)
     |   case Mul(l, r) => eval(l) * eval(r)
     |   case Div(l, r) => eval(l) / eval(r)
     |   case Lit(v) => v
     | }
eval: (exp: Exp)Int

この定義をREPLに読み込ませて、eval(example)として、

scala> eval(example)
res1: Int = 4

のように表示されれば成功です。きちんと、1 + ((2 * 3) / 2)という式の計算結果が出ていますね。ここで注目すべきは、パターンマッチングによって、

  1. ノードの種類と構造によって分岐する
  2. ネストしたノードを分解する
  3. ネストしたノードを分解した結果を変数に束縛する

という3つの動作が同時に行えていることです。 これがケースクラスを使ったデータ型とパターンマッチングの組み合わせの強力さです。また、このmatch式の中で、たとえば case Lit(v) => vの行を書き忘れた場合、DayOfWeekの例と同じように、

<console>:16: warning: match may not be exhaustive.
It would fail on the following input: Lit(_)
       def eval(exp: Exp): Int = exp match {

記述漏れがあることを指摘してくれますから、ミスを防ぐこともできます。

変数宣言におけるパターンマッチング

match式中のパターンマッチングのみを扱ってきましたが、実は変数宣言でもパターンマッチングを行うことができます。

たとえば、次のようなケースクラスPointがあったとします。

scala> case class Point(x: Int, y: Int)
defined class Point

このケースクラスPointに対して、

scala> val Point(x, y) = Point(10, 20)
x: Int = 10
y: Int = 20

とすると、x10が、y20が束縛されます。もしパターンにマッチしなかった場合は、例外 scala.MatchError が発生してしまうので、変数宣言におけるパターンマッチングは、それが必ず成功すると型情報から確信できる場合にのみ使うようにしましょう。

練習問題

DayOfWeek型を使って、ある日の次の曜日を返すメソッドnextDayOfWeek

def nextDayOfWeek(d: DayOfWeek): DayOfWeek = ???

をパターンマッチを用いて定義してみましょう。

練習問題

二分木(子の数が最大で2つであるような木構造)を表す型TreeBranch, Emptyを考えます:

sealed abstract class Tree
case class Branch(value: Int, left: Tree, right: Tree) extends Tree
case object Empty extends Tree

子が2つで左の子の値が2、右の子の値が3、自分自身の値が1の木構造はたとえば次のようにして定義することができます。

scala> val tree: Tree = Branch(1, Branch(2, Empty, Empty), Branch(3, Empty, Empty))
tree: Tree = Branch(1,Branch(2,Empty,Empty),Branch(3,Empty,Empty))

このような木構造に対して、

  1. 最大値を求めるmaxメソッド:
  2. 最小値を求めるminメソッド:
  3. 深さを求めるdepthメソッド:
def max(tree: Tree): Int = ???
def min(tree: Tree): Int = ???
def depth(tree: Tree): Int = ???

をそれぞれ定義してみましょう。なお、

depth(Empty) == 0
depth(Branch(10, Empty, Empty)) == 1
depth(Branch(10, Branch(20,
                    Empty,
                    Empty
                 ), Empty)) == 2
// 右のBranchの方が、左のBranchよりも深い
depth(Branch(10, Branch(20,
                    Empty,
                    Empty
                 ), Branch(30,
                    Branch(40,
                        Empty,
                        Empty
                    ),
                 Empty))) == 3

です。

余裕があれば木構造を、

左の子孫の全ての値 <= 自分自身の値 < 右の子孫の全部の値

となるような木構造に変換するsortメソッド:

def sort(tree: Tree): Tree = ???

を定義してみましょう。なお、sortメソッドは、葉ノードでないノードの個数と値が同じであれば元の構造と同じでなくても良いものとします。

部分関数

これまでの説明の中で、無名関数とパターンマッチングについて説明してきましたが、この2つの機能を組み合わせた部分関数(PartialFunction)がScalaには存在します。説明の前に、まず、具体的なユースケースを挙げます:

scala> List(1, 2, 3, 4, 5).collect { case i if i % 2 == 1 => i * 2 }
res7: List[Int] = List(2, 6, 10)

ここで、collectメソッドは pf: PartialFunction[A, B] を引数に取り、pf.isDefinedAt(i)true になる要素のみを残し、さらに、pf.apply(i) の結果の値を元にした新しいコレクションを返します。

isDefinedAt

i % 2 == 1

の部分から自動的に生成され、パターンがマッチするときのみ真になるように定義されます。collectはこのisDefinedAt メソッドを使うことで、filtermap に相当する処理を同時に行うことができています。

PartialFunction は、自分でクラスを継承して作ることも可能ですが、一般的には、

{
  case pat1 => exp1
  case pat2 => exp2
  ...
  case patn => expn
}

という形の式から、自動的に生成されます。isDefinedAtが真になる条件は、pat1 ... patnのいずれかの条件にマッチすることです。

PartialFunctionを使う機会は実用的にはそれほど多いわけではありませんが、collectやサードパーティのライブラリで使うことがしばしばあるので、覚えておくと良いでしょう。

注意点として、 { case .... } という形式は、あくまで PartialFunction 型が要求されているときにのみ PartialFunction が生成されるのであって、通常の FunctionN 型が要求されたときには、違う意味を持つということです。たとえば、以下のような定義があったとします。

scala> val even: Int => Boolean = {
     |   case i if i % 2 == 0 => true
     |   case _ => false
     | }
even: Int => Boolean = $$Lambda$1793/1048652979@4357f2ef

このとき、この定義は、無名関数とパターンマッチを組み合わせたものと同じ意味になります。この点にだけ注意してください。

scala> val even: Int => Boolean = (x => x match {
     |   case i if i % 2 == 0 => true
     |   case _ => false
     | })
even: Int => Boolean = $$Lambda$1794/917539909@54af1131

results matching ""

    No results matching ""