minimize

事業拡大のため、新しい仲間を募集しています。
→詳しくはこちら

case class

Scala では、特殊な class として case class というものがある。

case class は、いわゆるデータクラスを作るときに便利なクラス。
このクラスは定義されたフィールドに応じて自動的に equals(), toString(), hashCode() が生成される。

case class Person(first:String, last:String, age:Int)

equals() は各フィールドをそれぞれ比較し、toString() は読みやすい形でPersonの内容を文字列化する。
また、変数の定義に val を書かなくても自動的に Getter が生成される。
もちろん var と書けば Setter も生成される。

このように何の関数も持たない純粋なデータクラスの場合、Scala ではクラス実体を省略できる。
つまり {} すらいらない。

case class Expr(s: String)
case class Plus(x: Int)

case class は、使うときにも便利な記述法がある。

val ted = Person("Ted", "Neward", 37)

このように、new を使わずに生成できる。
これは言ってみれば、Person オブジェクトが同名のFactory関数を持っていると考えれば良い。
もちろん内部では Person を new しているので、記述上の違いでしかない。
言い換えれば、この記法でオブジェクトを生成していればそれが case class だということがわかるかもしれない。

new Plus(new Number(5), new Number(8))

これより、

Plus(Number(5), Number(8))

こっちのがずっと読みやすいだろう?

パターンマッチング

case class を使う最大の理由。
それは Scala が持つ(非常に強力な)パターンマッチング機能を使えるから。
これは Java の switch を遥かに凌ぐものだ。

term match {
  case Num(x) => x
  case Plus(left, right) => eval(left) + eval(right)
  case _ => ()
}

match は Scala で予約されたキーワードである。
上記によって、term が Num 型ならばその値、term が Plus 型ならば
Plus が持つ2つのフィールドを足した値を返す。

このように、match では型によるマッチングやフィールドのバインドが簡単に行える。
もっと見てみよう。

case Plus(x, y) if x == y => ...
case Person(_, _, a) if a > 30 => ...
case Person(_, "Neward", _) => ...
case Person(first, last, ageInYears) if ageInYears > 17 => ...
case _ => ...

if による条件分岐、_ によるワイルドカード、固定値(上の例では"Neward")によるマッチングなど多彩。
match は上の case 節から順番にマッチングを適用していき、マッチングに成功すればただちにその値を返す。
つまり、複数の case にマッチする場合はより先頭に近い方の case 文が適用される。
そういうわけで、case 文を記述する順番は重要だ。
ちなみに、case 文の最後に break は必要ない。(そもそも Scala に break は存在しないが)

仮に2行目の例を Java で書いてみたらどうなるかやってみよう。

if (obj instanceof Person) {
  Person person = (Person)obj;
  int a = person.getAge();
  if (a > 30) { ... }
}

どちらが簡単で読みやすいか、一目瞭然だ。
今まで if の連続で書いていた処理を match で書き直してみたら、驚くほどコードがすっきりするだろう。
if や switch ではなく、match で。それが Scala 流。

sealed class

クラス定義に sealed を付けると、そのクラスは「同一ソースファイル以外の場所で継承することが禁止される」。
final[not at this source] とでもいったところか。

これにはどんな効用があるかというと、コンパイラがパターンマッチング時に警告を出してくれるというものがある。
例えば、Option[T] は以下のように定義されている。

sealed abstract class Option[+A]
final case class Some[+A](x: A) extends Option[A]
case object None extends Option[Nothing]

Option が sealed 宣言されていて、そこから Some, None という2クラスが定義されている。
Scala はこの時「Option のサブクラスは Some, None だけであり、他には存在しない」
ということを認識できる。

こんなとき、以下のコードを書くとコンパイラは警告(エラーではない)を出してくれる。

def f(x: Option[Int]) = x match {
  case Some(y) => y
}

Option のサブクラスとして None があるのに、それが match 句に現れていないからだ。
match 句は一般的に全ての値に対して戻り値を返す必要がある。
上の例では、もし x が None だった場合にこのコードは「実行時例外を吐く」。

ちなみに、この警告を敢えて出したくない場合は以下のような Annotation を付ける。

def f(x: Option[Int]) = (x: @unchecked) match {
  case Some(y) => y
}

これは PartialFunction などで有効な記法。