それが僕には楽しかったんです。

いろんなレイヤーに居ます

全てのプログラマに捧げるScala入門 コレクション

はじめに

この記事ではScalaのコレクションについて解説する。
しかし、コレクションをすべて隅から隅まで紹介するにはこの一つの記事では難しいため
特によく使うものに厳選して解説する

コレクション

コレクションにはmutable(可変)とimmutable(不変)のものが存在するため
まずそれらについて解説し、その後にそれらを踏まえた性能の特性について解説する。
特に、immutableであるコレクションを使う際にはそれぞれの性能特性を理解しておくことが
パフォーマンス的に重要になってくる。

不変と可変

不変と可変はこれ以前の記事で解説した、valとvarに似たとようなものである。
しかし、コレクションとこれらを併用することで微妙な差が出てくる。
ここでは特にそれを紹介する。

コレクションにおける不変と可変

次のようなimmutableなリストを例として挙げる。

val list: List[Int] = List(1,2,3,4,5)

このリストに対して、インデックスを使用して値を参照する場合は
applyメソッドか()でインデックスを指定する。

list(0) //return -> 1
list.apply(1) //return -> 2

インデックスを取得できるなら値の更新ができるはずだが、immutableリストではそうならない。

scala> list(0) = 10
<console>:9: error: value update is not a member of List[Int]
              list(0) = 10
              ^

エラーにも出ているがimmutableなコレクション、今回の場合で言えば特にイミュータブルリストは
updateという値の更新をするメソッドが存在しないために更新できない。
immutableとは内部の値も更新できない、つまりは内部状態が不変なコレクションと言える。

それでは値を更新したい場合はどうするのか。

解決策は至って簡単でListを作りなおすことである。
先頭に要素を追加する場合と末尾に要素を追加する場合の2つを紹介する。

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

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

このように::と:+を使うことでそれぞれ先頭追加、末尾追加が行える。
そして、値を追加した後、新しいリストが返ってきている。

しかし、これを次のように再代入はできない。

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

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

というのも、listという変数は今valで宣言されており
値を追加したimmutableなリストは新しいListオブジェクトとなっているので
valにおいては参照するオブジェクトが変更されることに等しいので再代入はできない。
なので、実際に上記のコードのように再代入したいのなら変数はvarで宣言する必要がある。


それでは次にmutableリストについて解説する。
immutableリストを解説したあとだとこちらはわかりやすいかもしれない。

まずmutableリストを使うにはcollection.mutableパッケージをインポートする必要がある。

import scala.collection.mutable.ListBuffer

mutableなリストはMutableListが存在するが実際にはListBufferを使うことが多いと思うので
今回はそちらをインポートする。
そして先ほどと同様にリストを宣言する。

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

valで宣言していことを覚えておきながら値の更新を行う。

//index = 0の値を2に更新
scala> list.update(0, 2)

scala> list.foreach(println _)
2
2
3
4
5

更新が正常に終了している。
valで宣言したmutableコレクションの値を更新することが出来たのは
コレクション、今回ならListBufferのオブジェクト自体に変更はなく
変更されたのはコレクションの内部状態であるからvalでも変更ができた。

これと同様に、先頭や末尾への追加がおこなえる。

性能特性

性能特性に関しては、いちからまとめるより公式のドキュメントがよくまとめているのでそちらを紹介する。

性能特性 | Scala Documentation


特に気をつけたいのは線形と定数となっている部分である。

例えばimmutableなListでは末尾への追加が線形になっていて先頭への追加が定数となっている。
これが意味することは末尾への追加はその要素数分だけコストがかかり、先頭への追加は一定のコストしかかからないということである。

もし要素数が多いリストに値を追加するなら最初から先頭追加にして、
追加が完了した段階でreverseなどをつかって逆順にすることの方が賢い方法と言える。

シーケンス

ここでは特によく使うシーケンスのコレクションについてのみ解説する。
シーケンスとは要素が順序をもっておりインデックス等で要素を指定できるコレクションのことである。
先ほど紹介したListもシーケンス型のコレクションに含まれている。

リスト

Scalaにおけるリストは次のような構造になっている

List(1,2,3, ..., n)

これをもっと掘り下げると次のような構造になっている。

1 :: 2 :: 3 :: ... :: n :: Nil

先頭追加のメソッドが連続した状態で、末尾がかならずNilになっている。
データ型については前章を見てもらうとわかるが型推論でIntになっている。
これが例えばIntとDoubleが混在したリストだとどうなるだろうか?

List(1,1.0)
res4: List[Double] = List(1.0, 1.0)

IntはDoubleにキャストできるためDoubleのリストとして解釈され、Intの要素もDoubleに変換されている。

では次にIntとStringの混在リストはどうなるだろうかやってみよう。

scala> List(1, "hello")
res5: List[Any] = List(1, hello)

すべてのクラスの親クラスであるAny型に推論されている。
推論はされているが、実際はこのようにいくつかの型をまとめるようなデータ構造は避けるべきである。

次にリストのなかでよく使うメソッドを一気にコードとして紹介する。

//Listでは無いものをListに変換する
scala> val list = (1 to 10).toList
list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

//先頭要素を求める
scala> list.head
res0: Int = 1

//先頭以外を求める
scala> list.tail
res1: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10)

//サイズを求める、sizeでもいい
scala> list.length
res2: Int = 10

//空かどうかを求める
scala> list.isEmpty
res3: Boolean = false

//末尾要素を求める
scala> list.last
res4: Int = 10

//末尾以外を求める
scala> list.init
res5: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)

//先頭からn分だけリストにする
scala> list.take(5)
res6: List[Int] = List(1, 2, 3, 4, 5)

//末尾からn分だけリストにする
scala> list.takeRight(5)
res7: List[Int] = List(6, 7, 8, 9, 10)

//先頭の要素n個を除いたものを求める
scala> list.drop(5)
res8: List[Int] = List(6, 7, 8, 9, 10)

//末尾の要素n個を除いたものを求める
scala> list.dropRight(5)
res9: List[Int] = List(1, 2, 3, 4, 5)

//引数が含まれているか求める
scala> list.contains(5)
res11: Boolean = true

//逆順にする
scala> list.reverse
res12: List[Int] = List(10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

//最大値
scala> list.max
res13: Int = 10

//最小値
scala> list.min
res14: Int = 1
配列

リストだけでなく当然配列も存在する。
配列は要素のシーケンスを保持することができて、任意の要素に対して一定のコストでアクセスできる。

一般的な配列はScalaだと次のようにかける。

//要素数5で作成
val array = new Array[Int](5)

もちろんListのようにパラメータでも初期化できる。
メソッドに関してもほとんどListと同じで大きな違いといえば性能面になる

バッファ

最初に紹介したListBufferと同様に、mutableな配列やシーケンスは大抵****Bufferの名前え存在する。
これは内部状態がミュータブルであるが、利用できるメソッドとして大きな違いはない。

コレクションと関数

ここではリストの要素に関数を適用して新しいリストを求める場合について行う。
前の記事で紹介したラムダ式を多用することになる。

map

これは新しいリストを返す制御構造のなかで特に使うものであり次のように使用する。

scala> val list = 1 to 10 toList
list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

scala> list.map(num => num * 2)
res0: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

関数にたいして要素を一つずつ適用し、返ってきた値を新しいリストとして生成しているのがわかる。
この程度であればわざわざ明示的にラムダを書く必要がなくワイルドカードを使用して簡潔にかくことができる。

scala> val list = 1 to 10 toList
warning: there was one feature warning; re-run with -feature for details
list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

scala> list.map(_ * 2)
res0: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

filter

これは評価式をもつ関数を渡し、それに対して要素が真であるmのをリストとして返すものである。
使い方自体はmapと同じになる。

scala> list.filter(num => num % 2 != 0)
res1: List[Int] = List(1, 3, 5, 7, 9)

foreach

これはリストの要素に対して、繰り返し処理をする時に使う。
よく使うのは要素の表示など

scala> list.foreach(println _)
1
2
3
4
5
6
7
8
9
10

ただし、新たなコレクションが返ってくるわけではない。


おわりに

ここではまだ紹介していないMapやSet、Tuppleなどのコレクションや
畳み込みなどのメソッドがあるので、Scalaをもっと使いこなすにはそれらへの理解も必要になるだろう。

次はパターンマッチについての紹介になる。