読者です 読者をやめる 読者になる 読者になる

新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:ドワンゴ 新人向けScala研修テキスト

学習記録 関数型 プログラミング Scala

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

パターンマッチングはCやJavaのswitch文に似ているが,より強力な機能である。
パターンマッチングの真価を発揮するには,ケースクラスによるデータ型の定義が必要になる。

sealed abstract class DayOfWeek
case object Sunday extends DayOfWeek
case object Monday extends DayOfWeek
...
case object Saturday extends DayOfWeek

上記は曜日を表すデータ型で,CやJavaenumと同じように使うことができる。

scala> val x: DayOfWeek = Sunday
x: DayOfWeek = Sunday

objectまたはその他のデータ型は,パターンマッチングのパターンを使うことができる。
この例では,DayOfWeek型を継承した各objectをパターンマッチングのパターンに使うことができる。

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

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

これだけだとenumとあまり変わらないように見えるが,各々のデータは独立してパラメータを持つことができること,パターンマッチの際にデータを分解することができるのが特徴である。
例えば,四則演算を表す構文木を考える。各ノードExpを継承し,二項演算を表すノードは子として左辺lhsと右辺をrhsを持つ。

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

葉ノードとして整数リテラルを表すLitも作成する。これはIntの値を取る。
この定義から,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

この関数にexampleを渡すと,解として4が返ってくる。

scala> eval(example)
res25: Int = 4

ここで注目すべきは,パターンマッチによって,

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

という3つの動作が同時に行えていることである。これがケースクラスを使ったデータ型とパターンマッチングの組み合わせの強力さになる。

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

変数宣言でもパターンマッチングを行うことができる。

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

とすると,xとyに10と20が束縛される。