コレクションライブラリ(immutableとmutable)
Scalaには配列(Array
)やリスト(List
)、連想配列(Map
)、集合(Set
)を扱うための豊富なライブラリがあります。これを使いこなすことで、Scalaでのプログラミングは劇的に楽になります。注意しなければならないのは、Scalaでは一度作成したら変更できない(immutable)なコレクションと変更できる通常のコレクション(mutable)があることです。皆さんはmutableなコレクションに馴染んでいるかと思いますが、Scalaで関数型プログラミングを行うためには、immutableなコレクションを活用する必要があります。
immutableなコレクションを使うのにはいくつものメリットがあります
- 関数型プログラミングで多用する再帰との相性が良い
- 高階関数を用いて簡潔なプログラムを書くことができる
- 一度作ったコレクションが知らない箇所で変更されていない事を保証できる
- 並行に動作するプログラムの中で、安全に受け渡しすることができる
mutableなコレクションを効果的に使えばプログラムの実行速度を上げることができますが、mutableなコレクションをどのような場面で使えばいいかは難しい問題です。
この節では、Scalaのコレクションライブラリに含まれる以下のものについての概要を説明します。
Array
(mutable)List
(immutable)Map
(immutable)・Map
(mutable)Set
(immutable)・Set
(mutable)
Array
まずは大抵のプログラミング言語にある配列です。
val arr = Array(1, 2, 3, 4, 5)
// arr: Array[Int] = Array(1, 2, 3, 4, 5)
これで1から5までの要素を持った配列がarr
に代入されました。Scalaの配列は、他の言語のそれと同じように要素の中身を入れ替えることができます。配列の添字は0から始まります。なお、配列の型を指定しなくて良いのは、Array(1, 2, 3, 4, 5)
の部分で、要素型がInt
であるに違いないとコンパイラが型推論してくれるからです。型を省略せずに書くと
val arr = Array[Int](1, 2, 3, 4, 5)
// arr: Array[Int] = Array(7, 2, 3, 4, 5)
となります。ここで、[Int]
の部分は型パラメータと呼びます。Array
だけだとどの型かわからないので、[Int]
を付けることでどの型のArray
かを指定しているわけです。この型パラメータは型推論を補うために、色々な箇所で出てくるので覚えておいてください。しかし、この場面では、Array
の要素型はInt
だとわかっているので、冗長です。次に要素へのアクセスと代入です。
arr(0) = 7
arr
// res1: Array[Int] = Array(7, 2, 3, 4, 5)
arr(0)
// res2: Int = 7
他の言語だとarr[0]
のようにしてアクセスすることが多いので最初は戸惑うかもしれませんが、慣れてください。配列の0
番目の要素がちゃんと7
に入れ替わっていますね。
配列の長さはarr.length
で取得することができます。
arr.length
// res3: Int = 5
Array[Int]
はJavaではint[]
と同じ意味です。Scalaでは、配列などのコレクションの要素型を表記するとき
Collection[ElementType]
のように一律に表記し、配列も同じように記述するのです。Javaでは配列型だけ特別扱いするのに比べると統一的だと言えるでしょう。
ただし、あくまでも表記上はある程度統一的に扱えますが、実装上はJVMの配列であり、 要素が同じでもequalsの結果がtrueにならない, 生成する際にClassTagというものが必要 などのいくつかの罠があるので、Arrayはパフォーマンス上必要になる場合以外はあまり積極的に使うものではありません。
練習問題
配列のi
番目の要素とj
番目の要素を入れ替えるswapArray
メソッドを定義してみましょう。swapArray
メソッドの宣言は
def swapArray[T] (arr: Array[T])(i: Int, j: Int): Unit = ???
となります。i
とj
が配列の範囲外である場合は特に考慮しなくて良いです。
Range
Range
は範囲を表すオブジェクトです。Range
は直接名前を指定して生成するより、to
メソッドとuntil
メソッドを用いて呼びだすことが多いです。また、toList
メソッドを用いて、その範囲の数値の列を後述するList
に変換することができます。では、早速REPLでRange
を使ってみましょう。
1 to 5
// res8: Range.Inclusive = Range(1, 2, 3, 4, 5)
(1 to 5).toList
// res9: List[Int] = List(1, 2, 3, 4, 5)
1 until 5
// res10: Range = Range(1, 2, 3, 4)
(1 until 5).toList
// res11: List[Int] = List(1, 2, 3, 4)
to
は右の被演算子を含む範囲を、until
は右の被演算子を含まない範囲を表していることがわかります。また、Range
はtoList
で後述するList
に変換することができることもわかります。
List
さて、導入として大抵の言語にあるArray
を出しましたが、ScalaではArray
を使うことはそれほど多くありません。代わりにList
や
Vector
といったデータ構造をよく使います(Vector
については後述します)。List
の特徴は、一度作成したら中身を変更できない(immutable)ということです。中身を変更できないデータ構造(永続データ構造とも呼びます)はScalaがサポートしている関数型プログラミングにとって重要な要素です。それではList
を使ってみましょう。
val lst = List(1, 2, 3, 4, 5)
// lst: List[Int] = List(1, 2, 3, 4, 5)
lst(0) = 7
見ればわかるように、List
は一度作成したら値を更新することができません。しかし、List
は値を更新することができませんが、あるList
を元に新しいList
を作ることができます。これが値を更新することの代わりになります。以降、List
に対して組み込みで用意されている各種操作をみていくことで、List
の値を更新することなく色々な操作ができることがわかるでしょう。
Nil:空のList
まず最初に紹介するのはNil
です。Scalaで空のList
を表すにはNil
というものを使います。Rubyなどではnil
は言語上かなり特別な意味を持ちますが、Scalaではデフォルトでスコープに入っているということ以外は特別な意味はなく単にobjectです。Nilは単体では意味がありませんが、次に説明する::
と合わせて用いることが多いです。
:: - Listの先頭に要素をくっつける
::
(コンスと読みます)は既にあるList
の先頭に要素をくっつけるメソッドです。これについては、REPLで結果をみた方が早いでしょう。
val a1 = 1 :: Nil
// a1: List[Int] = List(1)
val a2 = 2 :: a1
// a2: List[Int] = List(2, 1)
val a3 = 3 :: a2
// a3: List[Int] = List(3, 2, 1)
val a4 = 4 :: a3
// a4: List[Int] = List(4, 3, 2, 1)
val a5 = 5 :: a3
// a5: List[Int] = List(5, 3, 2, 1)
付け足したい要素を::
を挟んでList
の前に書くことでList
の先頭に要素がくっついていることがわかります。ここで、::
はやや特別な呼び出し方をするメソッドであることを説明しなければなりません。まず、Scalaでは1引数のメソッドは中置記法で書くことができます。それで、1 :: Nil
のように書くことができるわけです。次に、メソッド名の最後が:
で終わる場合、被演算子の前と後ろをひっくり返して右結合で呼び出します。たとえば、
1 :: 2 :: 3 :: 4 :: Nil
// res12: List[Int] = List(1, 2, 3, 4)
は、実際には、
Nil.::(4).::(3).::(2).::(1)
// res13: List[Int] = List(1, 2, 3, 4)
のように解釈されます。List
の要素が演算子の前に来て、一見数値のメソッドのように見えるのにList
のメソッドとして呼び出せるのはそのためです。
++:List同士の連結
++
はList同士を連結するメソッドです。これもREPLで見た方が早いでしょう。
List(1, 2) ++ List(3, 4)
// res14: List[Int] = List(1, 2, 3, 4)
List(1) ++ List(3, 4, 5)
// res15: List[Int] = List(1, 3, 4, 5)
List(3, 4, 5) ++ List(1)
// res16: List[Int] = List(3, 4, 5, 1)
++
は1引数のメソッドなので、中置記法で書いています。また、末尾が:
で終わっていないので、たとえば、
List(1, 2) ++ List(3, 4)
// res17: List[Int] = List(1, 2, 3, 4)
は
List(1, 2).++(List(3, 4))
// res18: List[Int] = List(1, 2, 3, 4)
と同じ意味です。大きなList
同士を連結する場合、計算量が大きくなるのでその点には注意した方が良いです。
mkString:文字列のフォーマッティング
このメソッドはScalaで非常に頻繁に使用されます。皆さんも、Scalaを使っていく上で使う機会が多いであろうメソッドです。このメソッドは引数によって多重定義されており、3バージョンあるのでそれぞれを紹介します。
mkString
引数なしバージョンです。このメソッドは、単にList
の各要素を左から順に繋げた文字列を返します。
List(1, 2, 3, 4, 5).mkString
// res19: String = "12345"
注意しなければならないのは、引数なしメソッドのmkString
は()
を付けて呼びだすことができない
という点です。たとえば、以下のコードは、若干分かりにくいエラーメッセージがでてコンパイルに失敗します。
List(1, 2, 3, 4, 5).mkString()
Scalaの0
引数メソッドは()
なしと
()
を使った定義の二通りあって、前者の形式で定義されたメソッドは()
を付けずに呼び出さなければいけません。同様に、()
を使って定義されたメソッドは、()
を付けて呼び出さなければいけません。このScalaの仕様は混乱しやすいので注意してください。
mkString(sep: String)
引数にセパレータ文字列sep
を取り、List
の各要素をsep
で区切って左から順に繋げた文字列を返します。
List(1, 2, 3, 4, 5).mkString(",")
// res20: String = "1,2,3,4,5"
mkString(start: String, sep: String, end: String)
mkString(sep)
とほとんど同じですが、start
とend
に囲まれた文字列を返すところが異なります。
List(1, 2, 3, 4, 5).mkString("[", ",", "]")
// res21: String = "[1,2,3,4,5]"
練習問題
mkString
を使って、最初の数start
と最後の数end
を受け取って、
start,(start+1),(start+2)...,end
となるような文字列を返すメソッドjoinByComma
を定義してみましょう(ヒント:Range
にもmkString
メソッドはあります)。
例
joinByComma(1,5) // 1,2,3,4,5
def joinByComma(start: Int, end: Int): String = {
???
}
foldLeft:左からの畳み込み
foldLeft
メソッドはList
にとって非常に基本的なメソッドです。他の様々なメソッドをfoldLeft
を使って実装することができます。foldLeft
の宣言をScalaのAPIドキュメントから引用すると、
def foldLeft[B](z: B)(f: (B, A) => B): B
となります。z
がfoldLeft
の結果の初期値で、リストを左からたどりながらf
を適用していきます。foldLeft
についてはイメージが湧きにくいと思いますので、List(1, 2, 3).foldLeft(0)((x, y) => x + y)
の結果を図示します。
+
/ \
+ 3
/ \
+ 2
/ \
0 1
この図で、
+
/ \
0 1
は+
に0と1を与えて適用するということを意味します。リストの要素を左から順にfを使って「畳み込む」(fold
は英語で畳み込むという意味を持ちます)状態がイメージできるでしょうか。foldLeft
は汎用性の高いメソッドで、たとえば、List
の要素の合計を求めたい場合は
List(1, 2, 3).foldLeft(0)((x, y) => x + y)
// res23: Int = 6
List
の要素を全て掛けあわせた結果を求めたい場合は
List(1, 2, 3).foldLeft(1)((x, y) => x * y)
// res24: Int = 6
とすることで求める結果を得ることができます1。その他にも様々な処理をfoldLeft
を用いて実装することができます。
さて、節の最後に、実用上の補足を少ししておきます。少し恣意的ですが1つの例として、「リストのリスト」をリストに変換する(平らにする)処理というのを考えてみます。
List(List(1), List(2 ,3))
をList(1, 2, 3)
に変換するのが目標です。安直に書くとこうなるでしょうか:
scala> List(List(1), List(2, 3), List(4)).foldLeft(Nil)(_ ++ _)
<console>:12: error: type mismatch;
found : List[Int]
required: scala.collection.immutable.Nil.type
List(List(1), List(2, 3), List(4)).foldLeft(Nil)(_ ++ _)
^
しかしコンパイルが通りません。エラーメッセージの意味としては、今回のNil
はList[Int]
型と見なされてほしいわけですが、期待したように型推論できていないようです。
Nil
に明示的に型注釈を付けることで、コンパイルできるようになります。
List(List(1), List(2, 3), List(4)).foldLeft(Nil: List[Int])(_ ++ _)
// res25: List[Int] = List(1, 2, 3, 4)
このように、Nil
が混ざった処理はそのままだとうまくコンパイルが通ってくれないことがあります。そういう場合は型注釈を試すとよい、と頭の片隅に入れておいてください。
練習問題
foldLeft
を用いて、List
の要素を反転させる次のシグニチャを持ったメソッドreverse
を実装してみましょう:
def reverse[T](list: List[T]): List[T] = ???
foldRight:右からの畳み込み
foldLeft
がList
の左からの畳み込みだったのに対して、foldRight
は右からの畳込みです。foldRight
の宣言を
ScalaのAPIドキュメントから参照すると、
def foldRight[B](z: B)(op: (A, B) => B): B
となります。foldRight
に与える関数であるop
の引数の順序がfoldLeft
の場合と逆になっている事に注意してください。
foldRight
をList(1, 2, 3).foldRight(0)((y, x) => y + x)
とした場合の様子を図示すると次のようになります。
+
/ \
1 +
/ \
2 +
/ \
3 0
ちょうどfoldLeft
と対称になっています。foldRight
も非常に汎用性の高いメソッドで、多くの処理をfoldRight
を用いて実装することができます。
練習問題
List
の全ての要素を足し合わせるメソッドsum
をfoldRight
を用いて実装してみましょう。sum
の宣言は次のようになります。なお、List
が空のときは0を返してみましょう。
def sum(list: List[Int]): Int = ???
練習問題
List
の全ての要素を掛け合わせるメソッドmul
をfoldRight
を用いて実装してみましょう。mul
の宣言は次のようになります。なお、List
が空のときは1を返してみましょう。
def mul(list: List[Int]): Int = ???
練習問題
mkString
を実装してみましょう。mkString
そのものを使ってはいけませんが、foldLeft
やfoldRight
などのList
に定義されている他のメソッドは自由に使って構いません。ListのAPIリファレンス
を読めば必要なメソッドが載っています。実装するmkString
の宣言は
def mkString[T](list: List[T])(sep: String): String = ???
となります。残りの2つのバージョンのmkString
は実装しなくても構いません。
map:各要素を加工した新しいList
を返す
map
メソッドは、1引数の関数を引数に取り、各要素に関数を適用した結果できた要素からなる新たなList
を返します。ためしにList(1, 2, 3, 4, 5)
の各要素を2倍してみましょう。
List(1, 2, 3, 4, 5).map(x => x * 2)
// res30: List[Int] = List(2, 4, 6, 8, 10)
x => x * 2
の部分は既に述べたように、無名関数を定義するための構文です。メソッドの引数に与える短い関数を定義するときは、
Scalaでは無名関数をよく使います。List
の全ての要素に何らかの処理を行い、その結果を加工するという処理は頻出するため、map
は
Scalaのコレクションのメソッドの中でも非常によく使われるものになっています。
練習問題
次のシグニチャを持つmap
メソッドをfoldLeft
とreverse
を使って実装してみましょう:
def map[T, U](list: List[T])(f: T => U): List[U] = ???
map
メソッドは次のようにして使います。
assert(List(2, 3, 4) == map(List(1, 2, 3))(x => x + 1))
assert(List(2, 4, 6) == map(List(1, 2, 3))(x => x * 2))
assert(Nil == map(List[Int]())(x => x * x))
assert(List(0, 0, 0) == map(List(1, 2, 3))(x => 0))
filter:条件に合った要素だけを抽出した新しいList
を返す
filter
メソッドは、Boolean
型を返す1引数の関数を引数に取り、各要素に関数を適用し、true
になった要素のみを抽出した新たなList
を返します。List(1, 2, 3, 4, 5)
から奇数だけを抽出してみましょう。
List(1, 2, 3, 4, 5).filter(x => x % 2 == 1)
// res31: List[Int] = List(1, 3, 5)
練習問題
次のシグニチャを持つfilter
メソッドをfoldLeft
とreverse
を使って実装してみましょう:
def filter[T](list: List[T])(f: T => Boolean): List[T] = ???
assert(List(2) == filter(List(1, 2, 3))(x => x % 2 == 0))
assert(List(1, 3) == filter(List(1, 2, 3))(x => x % 2 == 1))
assert(Nil == filter(List(1, 2, 3))(x => x > 3))
assert(List(1) == filter(List(1))(x => x == 1))
assert(Nil == filter(List[Int]())(x => false))
find:条件に合った最初の要素を返す
find
メソッドは、Boolean
型を返す1引数の関数を引数に取り、各要素に前から順番に関数を適用し、最初にtrueになった要素を
Some
でくるんだ値をOption
型として返します。1つの要素もマッチしなかった場合None
をOption
型として返します。
List(1, 2, 3, 4, 5)
から最初の奇数だけを抽出してみましょう
List(1, 2, 3, 4, 5).find(x => x % 2 == 1)
// res32: Option[Int] = Some(value = 1)
後で説明されることになりますが、Option
型はScalaプログラミングの中で重要な要素であり頻出します。
練習問題
次のシグニチャを持つfind
メソッドをfoldLeft
または再帰で実装してみましょう。Option[T]
型のSome[T]
は Some(1)
のように生成できます。また、要素がないことを表すNone
はNone
と表記できます。
def find[T](list: List[T])(f: T => Boolean): Option[T] = ???
assert(Some(2) == find(List(1, 2, 3))(x => x == 2))
assert(None == find(List(1, 2, 3))(x => x > 3))
assert(Some(1) == find(List(1))(x => x == 1))
assert(None == find(List(1))(x => false))
assert(None == find(List[Int]())(x => x == 1))
takeWhile:先頭から条件を満たしている間を抽出する
takeWhile
メソッドは、Boolean
型を返す1引数の関数を引数に取り、前から順番に関数を適用し、結果がtrue
の間のみからなるList
を返します。List(1, 2, 3, 4, 5)
の5より前の4要素を抽出してみます。
List(1, 2, 3, 4, 5).takeWhile(x => x != 5)
// res33: List[Int] = List(1, 2, 3, 4)
練習問題
次のシグニチャを持つtakeWhile
メソッドをループまたは再帰を使って実装してみましょう:
def takeWhile[T](list: List[T])(f: T => Boolean): List[T] = ???
takeWhile
メソッドは次のようにして使います。
assert(List(1, 2, 3) == takeWhile(List(1, 2, 3, 4, 5))(x => x <= 3))
assert(List(1) == takeWhile(List(1, 2, 3, 3, 4, 5))(x => x == 1))
assert(List(1, 2, 3, 4) == takeWhile(List(1, 2, 3, 4, 5))(x => x < 5))
assert(Nil == takeWhile(List(1, 2, 3, 3, 2, 2))(x => false))
count:List
の中で条件を満たしている要素の数を計算する
count
メソッドは、Boolean
型を返す1引数の関数を引数に取り、全ての要素に関数を適用して、true
が返ってきた要素の数を計算します。例としてList(1, 2, 3, 4, 5)
の中から偶数の数(2になるはず)を計算してみます。
List(1, 2, 3, 4, 5).count(x => x % 2 == 0)
// res34: Int = 2
練習問題
次のシグニチャを持つcount
メソッドをfoldLeft
を使って実装してみましょう:
def count[T](list: List[T])(f: T => Boolean): Int = ???
count
メソッドは次のようにして使います。
assert(3 == count(List(1, 2, 3, 3, 2, 2))(x => x == 2))
assert(1 == count(List(1, 2, 3, 3, 2, 2))(x => x == 1))
assert(2 == count(List(1, 2, 3, 3, 2, 2))(x => x == 3))
assert(0 == count(List(1, 2, 3, 3, 2, 2))(x => x == 5))
flatMap:List
をたいらにする
flatMap
は一見少し変わったメソッドですが、後々重要になってくるメソッドなので説明しておきます。flatMapの宣言はScalaのAPIドキュメントから参照すると、
final def flatMap[B](f: (A) => IterableOnce[B]): List[B]
となります。ここで、IterableOnce[B]
という変わった型が出てきていますが、ここではあらゆるコレクション(要素の型はB型である)を入れることができる型程度に考えてください。さて、flatMapの引数fの型は(A) => IterableOnce[B]
です。flatMap
はこれを使って、各要素にfを適用して、結果の要素からなるコレクションを分解してListの要素にします。これについては、実際に見た方が早いでしょう。
List(List(1, 2, 3), List(4, 5)).flatMap{e => e.map{g => g + 1}}
// res35: List[Int] = List(2, 3, 4, 5, 6)
ネストしたList
の各要素にflatMap
の中でmap
を適用して、List
の各要素に1を足したものをたいらにしています。これだけだとありがたみがわかりにくいですが、ちょっと形を変えてみると非常に面白い使い方ができます:
List(1, 2, 3).flatMap{e => List(4, 5).map(g => e * g)}
// res36: List[Int] = List(4, 5, 8, 10, 12, 15)
List(1, 2, 3)
とList(4, 5)
の2つのList
についてループし、各々の要素を掛けあわせた要素からなるList
を抽出しています。実は、
for-comprehension
for(x <- col1; y <- col2) yield z
は
col1.flatMap{x => col2.map{y => z}}
のシンタックスシュガーだったのです。すなわち、ある自分で定義したデータ型にflatMap
とmap
を(適切に)実装すれば
for構文の中で使うことができるのです。
練習問題
次のシグニチャを持つflatMap
メソッドを再帰やループで実装してみましょう:
def flatMap[T, U](list: List[T])(f: T => List[U]): List[U] = ???
flatMap
メソッドは次のようにして使います。
assert(List(1, 2, 3) == flatMap(List(1, 2, 3))(x => List(x)))
assert(
List(3, 4, 6, 8) == flatMap(List(1, 2))(x =>
map(List(3, 4))(y => x * y)
)
)
Listの性能特性
List
の性能特性として、List
の先頭要素へのアクセスは高速にできる反面、要素へのランダムアクセスや末尾へのデータの追加は
List
の長さに比例した時間がかかってしまうということが挙げられます。List
は関数型プログラミング言語で最も基本的なデータ構造で、どの関数型プログラミング言語でもたいていはList
がありますが、その性能特性には十分注意して扱う必要があります。特に他の言語のプログラマはうっかりList
の末尾に要素を追加するような遅いプログラムを書いてしまうことがあるので注意する必要があります。
List(1, 2, 3, 4)
// res37: List[Int] = List(1, 2, 3, 4)
5 :: List(1, 2, 3, 4) // Listの先頭のセルに新しいをくっつける
// res38: List[Int] = List(5, 1, 2, 3, 4)
List(1, 2, 3, 4) :+ 5 // 注意!末尾への追加は、Listの要素数分かかる
// res39: List[Int] = List(1, 2, 3, 4, 5)
紹介したメソッドについて
mkString
をはじめとしたList
の色々なメソッドを紹介してきましたが、実はこれらの大半はList
特有ではなく、既に紹介したRange
やArray
、これから紹介する他のコレクションでも同様に使うことができます。何故ならばこれらの操作の大半は特定のコレクションではなく、コレクションのスーパータイプである共通のトレイト中に宣言されているからです。もちろん、List
に要素を加える処理とSet
に要素を加える処理(Set
に既にある要素は加えない)のように、中で行われる処理が異なることがあるので、その点は注意する必要があります。詳しくはScalaのAPIドキュメントを探索してみましょう。
Vector
Vector
は少々変わったデータ構造です。Vector
は一度データ構造を構築したら変更できないimmutableなデータ構造です。要素へのランダムアクセスや長さの取得、データの挿入や削除、いずれの操作も十分に高速にできる比較的万能なデータ構造です。immutableなデータ構造を使う場合は、まずVector
を検討すると良いでしょう。
Vector(1, 2, 3, 4, 5) //どの操作も「ほぼ」一定の時間で終わる
// res40: Vector[Int] = Vector(1, 2, 3, 4, 5)
6 +: Vector(1, 2, 3, 4, 5)
// res41: Vector[Int] = Vector(6, 1, 2, 3, 4, 5)
Vector(1, 2, 3, 4, 5) :+ 6
// res42: Vector[Int] = Vector(1, 2, 3, 4, 5, 6)
Vector(1, 2, 3, 4, 5).updated(2, 5)
// res43: Vector[Int] = Vector(1, 2, 5, 4, 5)
Map
Map
はキーから値へのマッピングを提供するデータ構造です。他の言語では辞書や連想配列と呼ばれたりします。
ScalaではMap
として一度作成したら変更できないimmutableなMap
と変更可能なmutableなMap
の2種類を提供しています。
scala.collection.immutable.Map
Scalaで何も設定せずにただMap
と書いた場合、scala.collection.immutable.Map
が使われます。その名の通り、一度作成すると変更することはできません。内部の実装としては主にscala.collection.immutable.HashMap
と
scala.collection.immutable.TreeMap
の2種類がありますが、通常はHashMap
が使われます。
val m = Map("A" -> 1, "B" -> 2, "C" -> 3)
// m: Map[String, Int] = Map("A" -> 1, "B" -> 2, "C" -> 3)
m.updated("B", 4) //一見元のMapを変更したように見えても
// res44: Map[String, Int] = Map("A" -> 1, "B" -> 4, "C" -> 3)
m // 元のMapはそのまま
// res45: Map[String, Int] = Map("A" -> 1, "B" -> 2, "C" -> 3)
scala.collection.mutable.Map
Scalaの変更可能なMap
はscala.collection.mutable.Map
にあります。実装としては、scala.collection.mutable.HashMap
、
scala.collection.mutable.LinkedHashMap
、リストをベースにしたscala.collection.mutable.ListMap
がありますが、通常は
HashMap
が使われます。
import scala.collection.mutable
val m = mutable.Map("A" -> 1, "B" -> 2, "C" -> 3)
// m: mutable.Map[String, Int] = HashMap("A" -> 1, "B" -> 5, "C" -> 3)
m("B") = 5 // B -> 5 のマッピングに置き換える
m // 変更が反映されている
// res47: mutable.Map[String, Int] = HashMap("A" -> 1, "B" -> 5, "C" -> 3)
Set
Set
は値の集合を提供するデータ構造です。Set
の中では同じ値が2つ以上存在しません。たとえば、Int
のSet
の中には1が2つ以上含まれていてはいけません。REPLでSet
を作成するための式を入力すると、
Set(1, 1, 2, 3, 4)
// res48: Set[Int] = Set(1, 2, 3, 4)
重複した1が削除されて、1が1つだけになっていることがわかります。
scala.collection.immutable.Set
Scalaで何も設定せずにただSet
と書いた場合、scala.collection.immutable.Set
が使われます。immutableなMap
の場合と同じく、一度作成すると変更することはできません。内部の実装としては、主に scala.collection.immutable.HashSet
と
scala.collection.immutable.TreeSet
の2種類がありますが、通常はHashSet
が使われます。
val s = Set(1, 2, 3, 4, 5)
// s: Set[Int] = HashSet(5, 1, 2, 3, 4)
s - 5 // 5を削除した後も
// res49: Set[Int] = HashSet(1, 2, 3, 4)
s // 元のSetはそのまま
// res50: Set[Int] = HashSet(5, 1, 2, 3, 4)
scala.collection.mutable.Set
Scalaの変更可能なSet
はscala.collection.mutable.Set
にあります。主な実装としては、scala.collection.mutable.HashSet
、
scala.collection.mutable.TreeSet
がありますが、通常はHashSet
が使われます。
import scala.collection.mutable
val s = mutable.Set(1, 2, 3, 4, 5)
// s: mutable.Set[Int] = HashSet(1, 2, 3, 4)
s -= 5 // 5 を削除したら
// res51: mutable.Set[Int] = HashSet(1, 2, 3, 4)
s // 変更が反映される
// res52: mutable.Set[Int] = HashSet(1, 2, 3, 4)
その他資料
さらにコレクションライブラリについて詳しく知りたい場合は、以下の公式のドキュメントなどを読みましょう https://docs.scala-lang.org/ja/overviews/collections/introduction.html
1. ただし、これはあくまでもfoldLeftの例であって、要素の和や積を求めたい場合に限って言えばもっと便利なメソッドが標準ライブラリに存在するので、実際にはこの例のような使い方はしません ↩