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

パターンマッチングは 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()

覚えておこう。

Range

Range は範囲を表すオブジェクトだ。to または until 演算子を使って作る。

scala> 1 to 5
res0: scala.collection.immutable.Range.Inclusive = Range 1 to 5

scala> 1 until 5
res1: scala.collection.immutable.Range = Range 1 until 5

toList メソッドを使ってリストにしたほうがわかりやすい。

scala> (1 to 5).toList
res2: List[Int] = List(1, 2, 3, 4, 5)

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

上に見えるように、to は右の被演算子を範囲に含み、until は含まない。