Option

今日から Scala のエラーを表すデータ型を見ていく。

最初は Option 型だ。Option は言ってみれば値を1つだけ入れられるコンテナで、値の入っている Some と何もないっていないことを表す None の2つの値がある。

っていうか、これ、OCmal の Option と同じだよね。Haskell で言えば Maybe だ。

Option は、たとえば次のように作れて、動作する。

scala> val o1: Option[String] = Option("hoge")
o1: Option[String] = Some(hoge)

scala> o1.isEmpty
res0: Boolean = false

scala> o1.isDefined
res1: Boolean = true

scala> o1.get
res2: String = hoge

Option はコンテナなので型パラメータを持つ。上の例では String だ。で、持っている値は get メソッドで取得できる。

じゃあ、null を入れた場合はどうだろう。

scala> val o2: Option[String] = Option(null)
 o2: Option[String] = None

scala> o2.isEmpty
res3: Boolean = true

scala> o2.isDefined
res4: Boolean = false

scala> o2.get
java.util.NoSuchElementException: None.get
  at scala.None$.get(Option.scala:349)
  at scala.None$.get(Option.scala:347)
  … 36 elided

null を入れると Option の値としては None になる。っていうか null ってなに?Java の null?

まあいいか。とにかく中身の値がないので、get メソッドではエラーになっている。

None も Option の値なので、isEmpty とか isDefined とかのメソッドに対してはちゃんと値を返している。もうひとつ、Option には便利な getOrElse メソッドがあって None だった場合には別の値を返すことができるようになっている。

scala> o2.getOrElse("no value")
res6: String = no value

最後に、パターンマッチを見よう。

scala> val s: Option[String] = Option("hoge")
s: Option[String] = Some(hoge)

scala> val result = s match {
     |     case Some(str) => str
     |     case None => "not matched"
     | }
result: String = hoge

Option は例外と違ってふつうのデータ型なので、パターンマッチができる。例外処理を書くんではなくて、ほかのデータ型と同じような処理をかけるわけだ。

部分関数

よくわからないものが出てきた。説明の前に使い方を見てみよう。

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

リストの collect メソッドは match 式の { } で囲まれたブロックの部分だけを引数にとって、条件に合う要素だけを抜き出し、=> の右の式を適用した新しいリストを返す(でいいのかな)。この引数が部分関数(PartialFunction)らしい。

引数、とさらっと言ったけど、引数はカッコに囲まれていない。あっちゃダメなのかと思ったらそうでもないようだ。

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

collect メソッドの動作はわかるんだけど、その引数が部分関数だといわれてもちょっとピンとこない。部分関数ってどうして部分?

もうちょっと厳密に言うと、引数に書いてあるブロック式自体は部分関数ではなくて、この式から部分関数が生成されるようだ。一般には case が複数あって、どれかのパターンにマッチしたときだけ新しい値がつくられてリストの要素になる。

てことは、こんなふうにも書けるわけだ。

scala> List(1,2,3,4,5).collect {
     |     case i if i % 2 == 0 => i * 2
     |     case i if i % 2 == 1 => i * i
     | }
res2: List[Int] = List(1, 4, 9, 8, 25)

うん、期待通りの動作はしてる。

でも、やっぱりなんで部分関数っていうんだかわからないなぁ。

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

ケースクラス(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 で書いたほうがわかりやすいかな。