ケースクラスによって自動生成されるもの

ケースクラス(case class )を宣言すると、ふつうのクラス(class)宣言に加えて、以下のものが追加で生成される。

  • プライマリコンストラクタ引数を公開する(クラス宣言の引数に val をつけたように動作する)
  • インスタンス同士の同値比較ができるように、equals()、hashCode()、canEqual()が定義される
  • 型とプライマリコンストラクタ引数を使った toString() が定義される
  • コンパニオンオブジェクトに apply() が定義される

ちょっと試してみよう。

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

プライマリコンストラクタ引数が公開されるので、x や y フィールドにアクセスできる。

scala> val p1 = new Point(1, 3)
p1: Point = Point(1,3)

scala> p1.x
res0: Int = 1

scala> p1.y
res1: Int = 3

コンパニオンオブジェクトに apply() が定義されるので、new キーワードなしでインスタンスを生成できる。

scala> val p2 = Point(1, 3)
p2: Point = Point(1,3)

すでに上の実行例で出てきているけど、toString() で見やすい形で出力される。

scala> p1.toString()
res2: String = Point(1,3)

同値比較。

scala> p1.equals(p2)
res3: Boolean = true

ハッシュコード。これはオブジェクトごとに一意に決まるハッシュ値みたいなものかな。

scala> p1.hashCode()
res4: Int = -1062267453

scala> p2.hashCode()
res5: Int = -1062267453

別のオブジェクトでも同値だとハッシュコードも同じになるみたいだ。

hasEqual() は使い方がわからなかった。

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

パターンマッチングは match 式の中だけじゃなく、変数宣言においても使用できる。

例えばケースクラス Point があったとして

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

こんなふうに変数宣言すると

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

変数 x と y に値が束縛される。

scala> x
res0: Int = 10

scala> y
res1: Int = 20

ちなみに、パターンがマッチしない場合には例外が発生するので、変数宣言でのパターンマッチングは型が合うことが確実な場合にだけ使うように、と資料には書いてある。

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

パターンマッチングの威力は、昨日見たような型による分岐にとどまらない。というのも各パターンは(ケースクラスがパラメータを持つならば)パラメータを持つこともできるからだ。これでほぼ Haskell のパターンマッチングと同様のことができる。

例として、整数の四則演算を表す構文木を考えよう。各ノードは Exp を継承し(つまりすべては式である)、2項演算を表す部分木(2つの子を持つ)と整数リテラルを表す葉からなる。各クラスの定義はこうだ。

scala> sealed abstract class Exp
defined class Exp

scala> case class Add(lhs: Exp, rhs: Exp) extends Exp
defined class Add

scala> case class Sub(lhs: Exp, rhs: Exp) extends Exp
defined class Sub

scala> case class Mul(lhs: Exp, rhs: Exp) extends Exp
defined class Mul

scala> case class Div(lhs: Exp, rhs: Exp) extends Exp
defined class Div

scala> case class Lit(value: Int) extends Exp
defined class Lit

これを組み合わせて、(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)))

さて、ここからが本番。この構文木を評価する評価器(メソッド)を作ろう。

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

このパターンマッチングでは、

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

ということがいっぺんに行われている。もちろん束縛した変数は評価に使うことができる。

さて、それじゃ上の構文木 example を評価してみよう。

scala> eval(example)
res2: Int = 4

ちゃんと (1 + ((2 * 3) / 2)) の計算結果である 4 が得られた。

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

ケースクラス(case class)は、Scala の強力なパターンマッチングのために必要なものらしい。普通のクラス(class)とどう違うのかよくわからないけど、パターンマッチングに使いたければケースクラスにしておけ、くらいに覚えておく(とりあえず)。

ケースクラスは次のように case キーワードをつけて宣言する。

scala> sealed abstract class DayOfWeek
defined class DayOfWeek

scala> case object Sunday extends DayOfWeek
defined object Sunday

scala> case object Monday extends DayOfWeek
defined object Monday

scala> case object Tuesday extends DayOfWeek
defined object Tuesday

scala> case object Wednesday extends DayOfWeek
defined object Wednesday

scala> case object Thursday extends DayOfWeek
defined object Thursday

scala> case object Friday extends DayOfWeek
defined object Friday

scala> case object Saturday extends DayOfWeek
defined object Saturday

クラスだと言っておきながらオブジェクト(object)なんだけど、どっちでもいいってことなんだろうか。ともかくこれらは次のようにパターンマッチングに使える。

scala> val x: DayOfWeek = Tuesday
x: DayOfWeek = Tuesday

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

曜日を表すオブジェクトに対応する整数を返すコードだけど、今日(x)は火曜日なので 2 が返ってきている。

警告が出ているのは、マッチの分岐が完全じゃないかもしれないってこと。実際 Saturday が抜けている。これは、スーパークラス/トレイトの宣言に sealed 修飾子をつけるとその(直接の)サブクラス/トレイト(オブジェクトも?)は同じファイル内にしか宣言できない、という性質を利用して実現されている。

sealed 修飾子はこの用途以外ではめったに使われないので、ケースクラスのスーパークラス/トレイトには sealed をつけておくものだと覚えておこう。

パターンマッチングはもっと強力なんだけど、今日のところはこのへんで。

セット

セット(Set)は集合を表すデータ構造だ。次のようにして作ることができる。

scala> val s = Set(1, 1, 2, 3, 3, 3, 4, 5)
s: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4)

コンストラクタの引数には 1 や 3 が重複しているけど、返ってきた値ではこの重複が取り除かれている。このようにセットは値の重複を許さない。あと、たぶん順番もないのだと思う。

セットにもイミュータブルなセットとミュータブルなセットがある。

イミュータブルなセット

上のように、単に Set(…) として作ったセットはイミュータブルなセット(scala.collection.immutable.Set)だ。要素を取り除いてみると

scala> s - 5
res4: scala.collection.immutable.Set[Int] = Set(1, 2, 3, 4)

一見取り除かれるように見けるけど、これは新しい値が返ってきているだけで、元の s は変わっていない。

scala> s
res5: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4)

ミュータブルなセット

ミュータブルなセット(scala.collection.mutable.Set)を作るには次のようにする。

scala> import scala.collection.mutable
import scala.collection.mutable

scala> val s2 = mutable.Set(1, 2, 3, 4, 5)
s2: scala.collection.mutable.Set[Int] = Set(1, 5, 2, 3, 4)

ここから 5 を取り除いてみよう。

scala> s2 -= 5
res7: s2.type = Set(1, 2, 3, 4)

scala> s2
res8: scala.collection.mutable.Set[Int] = Set(1, 2, 3, 4)

ミュータブルなセットだから、ちゃんと変更されているのがわかる。

マップ

マップ(Map)はいわゆる連想配列とか辞書とか呼ばれるデータ構造だ。

マップにはイミュータブルなマップと、ミュータブルなマップがある。なんで両方あるのかはわからない。

イミュータブルなマップ

Scala で単に Map と書くとイミュータブルなマップ(scala.collection.immutable.Map)になる。

scala> val m = Map("A" -> 1, "B" -> 2, "C" -> 3)
m: scala.collection.immutable.Map[String,Int] = Map(A -> 1, B -> 2, C -> 3)

イミュータブルだから変更はできない。次のようにすると変更できるように見えるけど

scala> m.updated("B", 4)
res0: scala.collection.immutable.Map[String,Int] = Map(A -> 1, B -> 4, C -> 3)

これは新しい値が返ってきているだけで、元の m は変更されていない。

scala> m
res1: scala.collection.immutable.Map[String,Int] = Map(A -> 1, B -> 2, C -> 3)

ミュータブルなマップ

ミュータブルなマップ(scala.collection.mutable.Map)を使うには、こうする。

scala> import scala.collection.mutable
import scala.collection.mutable

scala> val m2 = mutable.Map("A" -> 1, "B" -> 2, "C" -> 3)
m2: scala.collection.mutable.Map[String,Int] = Map(A -> 1, C -> 3, B -> 2)

キー B の値を変更してみよう。

scala> m2("B") = 5

すると、変数 m2 の値自体が変更されている。

scala> m2
res3: scala.collection.mutable.Map[String,Int] = Map(A -> 1, C -> 3, B -> 5)

リストのメソッド(3)flatMap

flatMap は、二重のリストの内側のリストを map して、結果をフラットなリストにするメソッドだ。

Ruby で書くとこう。

irb(main):001:0> [[1,2,3], [4,5]].map{|x| x.map{|y| y * y } }.flatten
=> [1, 4, 9, 16, 25]

この、外側の map と flatten をいっぺんにできる。

scala> List(List(1,2,3), List(4,5)).flatMap(x => x.map(y => y * y))
res0: List[Int] = List(1, 4, 9, 16, 25)

さらに、ちょっと工夫すると面白い使い方ができる。

scala> List(1,2,3).flatMap(x => List(10,20).map(y => x + y))
res1: List[Int] = List(11, 21, 12, 22, 13, 23)

外側のリスト List(1,2,3) と内側に現れるリスト List(10,20) とで、あたかも二十ループのようになっている。実際これはつぎの for-comprehension と同じだ。

scala> for (x <- List(1,2,3); y <- List(10,20)) yield x + y
res1: List[Int] = List(11, 21, 12, 22, 13, 23)

というよりも、for-comprehension というのは上の flatMap のシンタックスシュガーなのだそうだ。

じゃぁ、三重のループもできるんだろうか。

scala> List(1,2,3).flatMap(x => List(10,20).flatMap(y => List(100,200).map(z => x + y + z)))
res3: List[Int] = List(111, 211, 121, 221, 112, 212, 122, 222, 113, 213, 123, 223)

できた。

でも for で書いたほうがわかりやすいかな。

リストのメソッド(2)

今日はリストの高階関数を中心に見ていこう。

foldLeftとfoldRight

まずは畳み込み関数。foldLeft は左から、foldRight は右から畳み込む。

scala> List(1,2,3,4,5).foldLeft("0")((x, y) => List(x, y).mkString("(", ",", ")"))
res0: String = (((((0,1),2),3),4),5)
scala> List(1,2,3,4,5).foldRight("0")((x, y) => List(x, y).mkString("(", ",", ")"))
res1: String = (1,(2,(3,(4,(5,0)))))

畳み込みの初期値と関数が別の引数リストになってる。何のためだろ。

map

map は写像。

scala> List(1,2,3,4,5).map(x => x * x)
res2: List[Int] = List(1, 4, 9, 16, 25)

fileter

条件に合う要素だけを抜き出す。

scala> List(1,2,3,4,5).filter(x => x % 2 == 1)
res3: List[Int] = List(1, 3, 5)

find

条件に合う最初の要素を返す。

scala> List(1,2,3,4,5).find(x => x % 2 == 0)
res4: Option[Int] = Some(2)

Option[Int] って型と Some(2) って値が返ってきた。OCaml でいう Option 型かな。Haskell だと Maybe。

takeWhile

リストの先頭から条件に合っている間だけ抽出する。

scala> List(1,2,3,4,5).takeWhile(x => x < 3)
res5: List[Int] = List(1, 2)

count

条件に合う要素を数える。

scala> List(1,2,3,4,5).count(x => x % 2 == 0)
res6: Int = 2

リストのメソッド

今日からリスト(List)のメソッドを見ていく。

Nil

Nil は空のリストを表す。Lisp みたいだな。

scala> Nil
res0: scala.collection.immutable.Nil.type = List()

::(コンス)

リストの先頭に要素を付け足す演算子。

scala> 1 :: Nil
res1: List[Int] = List(1)

Scala の演算子っていうのは実はメソッドのシンタックスシュガーで、引数が1つのメソッドは中置記法で書ける。さらに : で終わる演算子は右側のメソッドとして解釈される。だから上の例は次のようにもかける。

scala> Nil.::(1)
res2: List[Int] = List(1)

まぁ、ふつうはこんな書きかたしないけどね。

scala> 1 :: List(2, 3, 4)
res3: List[Int] = List(1, 2, 3, 4)

scala> 1 :: 2 :: 3 :: 4 :: Nil
res4: List[Int] = List(1, 2, 3, 4)

連結

リスト同士を連結するには ++。Haskell といっしょだな。

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

mkString:文字列にフォーマッティングする

リストを文字列にする。mkString にはいくつかのバージョンがあって、まずは引数なしバージョン。これは単に要素を連結した文字列を返す。要素の型は文字列じゃなくてもいいみたいだ。

scala> List(1, 2, 3).mkString
res6: String = 123

次に引数1つのバージョン。引数をセパレータとして連結する。

scala> List(1, 2, 3).mkString("-")
res7: String = 1-2-3

最後に引数3つのバージョン。セパレータに加えて前後を囲む文字を指定する。

scala> List(1, 2, 3).mkString("<", "-", ">")
res8: String = <1-2-3>

さて、今回はこのくらいかな。次はリストの高階関数(メソッドだけど)を見ていこう。

[追記]

Nil のところで、REPL に現れる型が scala.collection.immutable.Nil.type になっている。

scala> Nil
res0: scala.collection.immutable.Nil.type = List()

Nil だけでは要素の型がわからないからこうなるみたいだ。要素の型を指定するにはこうする。

scala> Nil: List[String]
res1: List[String] = List()

覚えておこう。