こんにちは、 @kz_morita です。
Scala 入門 その6 ということでコレクション操作についてまとめます。
Scala に用意されている Collection
Scala の collection は以下のパッケージに所属しています。
scala.collection
scala.collection.mutable
scala.collection.immutable
scala.collection.generic
それぞれのパッケージの中にさまざまなトレイトやクラスが所属しています。
具体的にどんなものがあるかは、公式の
こちらのドキュメント
に詳しく記載されています。
Scala で代表的によく使うものとしては、Seq
, Set
, Map
当たりになると思います。
これらのよく使うものに関しては、エイリアスが用意されていて import などをしなくても使えるようになっています。
val seq = Seq(1,2,3,4)
これらはデフォルトで immutable なものが多く、mutable なものを使用したい場合には、以下のように scala.collection.mutable
を import して、mutable.Seq
のように使用することができます。
import scala.collection.mutable;
object Main extends App {
val seq = mutable.Seq(1,2,3,4)
}
この辺り、immutable な Collection がデフォルトになっているのはとても良さそうです。
上記の Seq ですがこちらはトレイトとなっていて、その実際の実装は List クラスになります。
val seq = Seq(1,2,3,4)
assert(seq.isInstanceOf[List[Int]])
scala-libaray の、scala.collection.immutable.Seq で以下のように、List が使用されていることがわかります。
/**
* $factoryInfo
* @define coll immutable sequence
* @define Coll `immutable.Seq`
*/
@SerialVersionUID(3L)
object Seq extends SeqFactory.Delegate[Seq](List) {
override def from[E](it: IterableOnce[E]): Seq[E] = it match {
case s: Seq[E] => s
case _ => super.from(it)
}
}
Initialize
コレクションの初期化は以下のように行います。
import scala.collection.mutable
object Main extends App {
val seq = Seq(1,2,3,4)
val mutableSeq = mutable.Seq(1,2,3,4)
val map = Map(1 -> "A", 2 -> "B", 3 -> "C")
val mutableMap = mutable.Map(1 -> "A", 2 -> "B", 3 -> "C")
val set = Set(1,2,3,4)
val mutableSet = mutable.Set(1,2,3,4)
}
filter
filter 系の関数は以下の通りです。
val seq = Seq(1,2,3,4,5)
// 偶数のみを抽出
seq.filter(x => x % 2 == 0) // => List(2, 4)
// 奇数のみ (偶数じゃない) を抽出
seq.filterNot(x => x % 2 == 0) // => List(1, 3, 5)
map
map 系の関数 (と partition) は以下の通りです。
val seq = Seq(1,2,3,4,5)
// x を 2倍する関数を各要素に適用
seq.map(x => x * 2) // List(2, 4, 6, 8, 10)
// 0 ~ x までのリストを 全要素分作成
seq.map(x => 0 to x) // List(Range 0 to 1, Range 0 to 2, Range 0 to 3, Range 0 to 4, Range 0 to 5)
// 上のリストを平坦化
seq.flatMap(x => 0 to x) // List(0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 5)
// x % 2 した値をキーにグループ化し、それぞれ 2倍する
seq.groupMap(x => x % 2)(x => x * 2) // Map(0 -> List(4, 8), 1 -> List(2, 6, 10))
// 偶数のグループと奇数のグループに分ける
seq.partition(x => x % 2 == 0) // (List(2, 4), List(1, 3, 5))
// 偶数のグループを 10 倍し、奇数のグループを 100 倍する
seq.partitionMap(x => if (x % 2 == 0) Left(x * 10) else Right(x * 100)) // (List(20, 40),List(100, 300, 500))
reduce
reduce 系の関数は以下の通りです。
// リストの左から、演算を行う
seq.reduce((acc, i) => acc - i) // -13
seq.reduceLeft((acc, i) => acc - i) // -13
// 1 2 3 4 5
// (1 - 2) 3 4 5
// -1 3 4 5
// (-1 - 3) 4 5
// -4 4 5
// (-4 - 4) 5
// -8 5
// (-8 - 5)
// -13
// リストの右から、演算を行う
seq.reduceRight((acc, i) => acc - i) // 3
// 1 2 3 4 5
// 1 2 3 (4 - 5)
// 1 2 3 -1
// 1 2 (3 - -1)
// 1 2 4
// 1 - (2 - 4)
// 1 - -2
// 3
// 初期値を指定してリストの左から演算
seq.fold(10)((acc, i) => acc - i) // -5
seq.foldLeft(10)((acc, i) => acc - i) // -5
// 10 1 2 3 4 5
// (10 - 1) 2 3 4 5
// 9 2 3 4 5
// (9 - 2) 3 4 5
// 7 3 4 5
// (7 - 3) 4 5
// 4 4 5
// (4 - 4) 5
// 0 5
// 0 - 5 = -5
// 初期値を指定してリストの右から計算
seq.foldRight(10)((acc, i) => acc - i) // -7
// 1 2 3 4 5 10
// 1 2 3 4 (5 - 10)
// 1 2 3 4 -5
// 1 2 3 (4 - -5)
// 1 2 3 9
// 1 2 (3 - 9)
// 1 2 -6
// 1 (2 - -6)
// 1 8
// 1 - 8
// -7
まとめ
Scala のおける Collection 操作周りを簡単にまとめました。
基本的な、map , filter , reduce は他の言語同様に使えるのでこの辺りをマスターしておくと良さそうです。
おまけ
おまけですが、興味があってコレクションのトレイトのデフォルト実装を調べてみていました。
Map のデフォルト実装が HashMap と思って、以下のように assert してみたのですがどうやら HashMap 以外のようだったのが不思議だったので、Scala のソースコードを読んでみました。
val map = Map(1 -> "A", 2 -> "B", 3 -> "C")
assert(map.isInstanceOf[HashMap[Int, String]]) // => assertion error
実装をみてみると、要素数が 1 ~ 4 の時はそれ専用の Map1 ~ Map4 クラスを代わりに使っているようで面白かったです。
final class Map1[K, +V](key1: K, value1: V)
extends AbstractMap[K, V] with StrictOptimizedIterableOps[(K, V), Iterable, Map[K, V]] with Serializable {}
final class Map2[K, +V](key1: K, value1: V, key2: K, value2: V)
extends AbstractMap[K, V] with StrictOptimizedIterableOps[(K, V), Iterable, Map[K, V]] with Serializable {}
class Map3[K, +V](key1: K, value1: V, key2: K, value2: V, key3: K, value3: V)
extends AbstractMap[K, V] with StrictOptimizedIterableOps[(K, V), Iterable, Map[K, V]] with Serializable {}
final class Map4[K, +V](key1: K, value1: V, key2: K, value2: V, key3: K, value3: V, key4: K, value4: V)
extends AbstractMap[K, V] with StrictOptimizedIterableOps[(K, V), Iterable, Map[K, V]] with Serializable {}
上記が、定義部分で、scala/collection/immutable/Map.scala
の、MapBuilderImpl
クラスにて、Map1 ~ 4 と HashMap の使い分けのコードが書かれています。
private[immutable] final class MapBuilderImpl[K, V] extends ReusableBuilder[(K, V), Map[K, V]] {
// ...
def addOne(key: K, value: V): this.type = {
if (switchedToHashMapBuilder) {
hashMapBuilder.addOne(key, value)
} else if (elems.size < 4) {
elems = elems.updated(key, value)
} else {
// assert(elems.size == 4)
if (elems.contains(key)) {
elems = elems.updated(key, value)
} else {
switchedToHashMapBuilder = true // => 5 つ目の要素が add されるタイミングで HashMap へスイッチするフラグをつけている
if (hashMapBuilder == null) {
hashMapBuilder = new HashMapBuilder
}
elems.asInstanceOf[Map4[K, V]].buildTo(hashMapBuilder)
hashMapBuilder.addOne(key, value)
}
}
this
}
// ...
}
実際に以下のように要素数によって、別々のクラスが使われていることが確認できました。
val map1 = Map(1 -> "A")
assert(map1.isInstanceOf[Map1[Int, String]])
val map2 = Map(1 -> "A", 2 -> "B")
assert(map2.isInstanceOf[Map2[Int, String]])
val map3 = Map(1 -> "A", 2 -> "B", 3 -> "C")
assert(map3.isInstanceOf[Map3[Int, String]])
val map4 = Map(1 -> "A", 2 -> "B", 3 -> "C", 4 -> "D")
assert(map4.isInstanceOf[Map4[Int, String]])
val map5 = Map(1 -> "A", 2 -> "B", 3 -> "C", 4 -> "D", 5 -> "E")
assert(map5.isInstanceOf[HashMap[Int, String]])
Scala の Map の実装見てるけど面白いな。
— kz_morita 𓆏 (@kz_morita) October 30, 2021
Map の要素数が 4以下の時は、Map1, Map2, Map3, Map4 っていうクラスが使われてて、要素数が 5以上になると、HashMap クラスになるのか。