新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。もう新人じゃないかも…

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

12. Scalaのコレクションライブラリ

Scalaには一度作成したら変更できないimmutableなコレクションと変更できる通常のmutableなコレクションがある。
Scala関数型プログラミングを行うためには,immutableなコレクションを活用する必要がある。

immutableなコレクションにはいくつものメリットがある。

  • 関数型プログラミングで多用する再帰との相性が良い
  • 高階関数を用いて簡潔なプログラムが書ける
  • 一度作ったコレクションが知らない箇所が変更されていないことを保証できる
  • 並行動作するプログラムで安全に受け渡しできる

この節では,Scalaのコレクションライブラリに含まれるArray, Range, List, Map, Setについて概要を説明する。

Array(mutable)

scala> val arr = Array(1, 2, 3, 4, 5)
arr: Array[Int] = Array(1, 2, 3, 4, 5)

配列の型を指定しなくていいのは,要素がIntであるとコンパイラ型推論してくれるため。
省略せずに書くとArray[Int](1,2,3,4,5)というようになる。

Scalaの配列は,他の言語のそれと同じように要素の中身を入れ替えることができ,添字は0から始まる。

scala> arr(0)
res3: Int = 1

scala> arr(0) = 7

scala> arr(0)
res5: Int = 7

scala> arr
res6: Array[Int] = Array(7, 2, 3, 4, 5)

他の言語だとarr[0]のようにアクセスすることが多いが,Scalaではarr(0)とすることに注意。
配列の長さはarr.lengthで取得することができる。

scala> arr.length
res7: Int = 5

Array[Int]はJavaでは int[]と同じ意味になる。
実装上はJVMの配列であり,要素が同じでもequalsの結果がtrueにならない,生成する際にClassTagというものが必要などいくつかの罠があるため,
Arrayはパフォーマンス上必要になる場合以外はあまり積極的に使うものではない。

Range(immutable)

Rangeは範囲を表すオブジェクトで,直接名前を指定して生成するより,toメソッドとuntilメソッドを用いて呼び出すことが多い。
また,toListメソッドでListに変換することもできる。

scala> 1 to 5
res10: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5)

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

scala> 1 until 5
res12: scala.collection.immutable.Range = Range(1, 2, 3, 4)

List(immutable)

ScalaではArrayを使うことはそれほど多くなく,代わりにListやVectorといったデータ構造をよく使用する。
Listの特徴は,一度作成したら中身を変更できない(immutable)ということである。

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

scala> lst(0) = 7
<console>:13: error: value update is not a member of List[Int]
       lst(0) = 7
       ^

上記のように,Listは一度作成したら値を更新することができない。
しかし,あるListを元に新しいListを作ることはできるので,これが値を更新することの代わりになる。

Nil:空のList

Scalaで空のListを表すにはNilというものを使う。
Scalaではデフォルトでスコープに入っているということ以外は特別ない見はなく,単なるobjectである。
Nilは単体では意味はないが,次に説明する::と合わせて用いることが多い。

:: :Listの先頭に要素をくっつける
(コンス)は既にあるListの先頭に要素をくっつけるメソッドである。
scala> val a1 = 1 :: Nil
a1: List[Int] = List(1)

scala> val a2 = 2 :: a1
a2: List[Int] = List(2, 1)

scala> val a3 = 3 :: a2
a3: List[Int] = List(3, 2, 1)

付け足したい要素を::を挟んでListの前に書くことで,Listの先頭に要素がくっついていることがわかる。
なお,::はやや特別な呼び出し方をするメソッドである。
まず,Scalaでは1引数のメソッドは中置記法で書くことができる。それで1 :: Nilのように書くことができる。
次に,メソッド名の最後が:で終わる場合,非演算子の前と後ろをひっくり返して右結合で呼び出す。
例えば,

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

は,実際には

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

のように解釈される。
Listの要素が演算子の前に来ており,一見数値のメソッドのように見えるにも関わらず,Listのメソッドとして呼び出せるのはこのためである。

++:List同士の連結
    1. はリスト同士を連結するメソッドである。
scala> List(1, 2) ++ List(3, 4)
res19: List[Int] = List(1, 2, 3, 4)

大きなList同士を連結する場合,計算量が大きくなるのでその点は注意が必要。

mkString:文字列のフォーマッティング

このメソッドはScalaで頻繁に利用され,使う機会が多いであろうメソッドである。
このメソッドは引数によって多重定義されており,3バージョンあるのでそれぞれを紹介する。

  • mkString

引数なしバージョンのメソッドは,単にListの各要素を左から順に繋げた文字列を返す。

scala> List(1,2,3,4,5).mkString
res20: String = 12345

注意が必要なのは,引数なしのmkStringは()を付けて呼び出すことができない点である。

scala> List(1,2,3,4,5).mkString()
<console>:12: error: overloaded method value mkString with alternatives:
  => String <and>
  (sep: String)String <and>
  (start: String,sep: String,end: String)String
 cannot be applied to ()
       List(1,2,3,4,5).mkString()

非常にわかりにくいエラーだが,Scalaの0引数メソッドは()なしと()を使った定義の二通りあり,前者の形式で定義されたメソッドは()を付けて呼び出すとこのエラーになってしまう。
逆に,()を使って定義されたメソッドは()を付けても付けなくても良いことになっている。

  • mkString(sep: Strng)

引数にセパレータ文字列sepを取るメソッド。

scala> List(1,2,3,4,5).mkString(",")
res22: String = 1,2,3,4,5
  • mkString(start: String, sep: String, end: String)

sepで区切り,startとendに囲まれた文字列を返す。

scala> List(1,2,3,4,5).mkString("[", ",", "]")
res23: String = [1,2,3,4,5]
foldLeft:左からの畳込み

foldLeftはListにとって非常に基本的なメソッドである。
他の様々なメソッドをfoldLeftを使って実装することができる。

foldLeftの宣言をScalaAPIドキュメントから引用すると,

def foldLeft[B](z: B)(f: (B, A) => B): B

となり,zがfoldLeftの結果の初期値で,リストを左からたどりながらfを適用していく。

scala> List(1,2,3).foldLeft(0)((x,y) => x + y)
res25: Int = 6

上記の例では,初期値を0から初めて,リストの要素を左から順に足していっている。
Listの要素を全て掛けあわせた結果を求めたい場合は以下のようになる。

scala> List(1,2,3).foldLeft(1)((x,y) => x * y)
res26: Int = 6
foldRight:右からの畳込み

foldLeftが左からの畳込みなのに対し,foldRightは右からの畳込みになる。
foldRightの宣言をScalaAPIドキュメントから引用すると,

def foldRight[B](z: B)(op: (A, B) => B): B

となる。foldRightに与える関数であるopの引数の順序がfoldLeftの場合と逆になることに注意。

map:各要素を加工した新しいListを返す

mapメソッドは,1引数の関数を引数に取り,各要素に関数を適用した結果できた要素からなるListを返す。

scala> List(1,2,3,4,5).map(x => x * 2)
res0: List[Int] = List(2, 4, 6, 8, 10)
find:条件に合った最初の要素を返す

Boolean型を返す1引数の関数を引数にとり,各要素に適用して最初にtrueになった要素をSomeでくるんだ値をOption型として返す。

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

scala> List(1,2,3,4,5).find(x => x % 6 == 0)
res4: Option[Int] = None
takeWhile:先頭から条件を満たしている間を抽出する

findと異なり,結果がtrueの間のみからなるListを返す。

scala> List(1,2,3,4,5).takeWhile(x => x != 3)
res5: List[Int] = List(1, 2)
count:Listの中で条件を満たしている要素の数を計算する

全ての要素に関数を適用して,trueが返ってきた要素の数を計算する。

scala> List(1,2,3,4,5).count(x => x < 4)
res6: Int = 3
flatMap:Listをたいらにする

flatMapの宣言は以下のとおり。

final def flatMap[B](f: (A) ⇒ GenTraversableOnce[B]): List[B]

GenTraversableOnceという変わった型は,ここではあらゆるコレクションを入れることができる型程度に考えておけば良い。
flatMapの引数fの型は(A) => GenTraversableOnce[B]で,これを使って各要素にfを適用していき,結果の要素からなるコレクションを分解してListの要素にする。

scala> List(List(1,2,3),List(4,5,6)).flatMap(e => e.map(g => g + 1))
res8: List[Int] = List(2, 3, 4, 5, 6, 7)

ネストしたListの各要素に,flatMapの中でmapを適用して,Listの各要素に1を足したものをたいらにしている。
これだけだと有り難みがわかりにくいが,少し形を変えると面白い使い方ができる。

scala> List(1,2,3).flatMap{e => List(4,5).map(g => e * g)}
res9: List[Int] = List(4, 5, 8, 10, 12, 15)

List(1,2,3)とList(4,5)の2つのListについてループし,各々の要素を掛けあわせた要素からなるListを抽出している。

Listの性能特性

Listの先頭要素へは高速にアクセスできる反面,ランダムアクセスや末尾へのデータ追加はListの長さに比例した時間がかかってしまう。
この性能特性には十分注意して扱う必要があり,特に他言語のプログラマはうっかりListの末尾に要素を追加するような遅いプログラムを書いてしまうので注意する必要がある。

Vector(immutable)

Vectorは少々変わったデータ構造で,要素へのランダムアクセスや長さの取得,データ挿入や削除などいずれの操作も十分に高速にできる比較的万能なデータ構造である。
immutableなデータ構造を使う場合は,まずVectorを検討すると良い。

Map(immutable/mutable)

Mapはキーから値へのマッピングを提供するデータ構造で,ScalaではimmutableなMapとmutableなMapの2種類を提供している。

scala.collection.immutable.Map

単にMapと書いた場合,こちらのimmutableなMapが使用される。
作成すると変更できず,updatedなどで更新したように見えても,更新したMapを返しているだけで,実際には元のMapは変更されない。

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

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

scala> m
res11: scala.collection.immutable.Map[String,Int] = Map(A -> 1, B -> 2)
|<<

*** scala.collection.mutable.Map

変更可能なMapを使うには,scala.collection.mutableにあるMapを使用する。

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

scala> m("B") = 5

scala> m
res13: scala.collection.mutable.Map[String,Int] = Map(A -> 1, B -> 5)

Set(immutable/mutable)

Setは値の集合を提供するデータ構造で,Setの中では同じ値が2つ以上存在しない。
重複した値を入れて生成しようとすると,重複した値は削除される。

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

何も指定せずSetと書いた場合,immutableなSetが使われる。

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

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

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

変更可能なSetはscala.collection.mutableにある。

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

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

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