【Seq / Map】Scala のコレクションについて調べてみた

| 0件のコメント

Spark アプリケーションを書くために Scala の勉強をしています。Python でも良いのですが Spark 自体が Scala で書かれていることもあって親和性は高いです。
参考にした本は Scalaスケーラブルプログラミング 第3版 第24章 コレクションの探求 で, Scalaコレクション の概要と基本となる Seq, Set, Map について書きます。

環境は macOS 10.12.3, Scala 2.12 です。

Scalaについて

Scala がどのような言語かは改めて書く必要はないけど, 振り返れるようにメモとして残しておく。
Scala はオブジェクト指向言語であり関数型言語でもある。オブジェクト指向言語なので全てはオブジェクトであり全ての操作はメソッド呼び出しとなる。またファーストクラス関数, 参照透過性, 不変性といった関数型言語の特徴も持っており, 関数型のスタイルで書くことが推奨されている。(valが普通, varは禁断の果実)
ただし Scala は選択を認めるので命令型のスタイルで書くこともできる。
性能面では Java のバイトコードになるので Java と同等の速度が期待できる。(ちなみに Java は書いたことないので全く知識がない…)
また Java と比較し抽象化されているのでコード量も削減できる。その代償なのかコンパイルは長く感じる。この点はLL言語に慣れていると気になってしまう。

Scalaコレクション

Scalaコレクションの特徴は以下。

  • 使いやすさ: 複雑なループ構造や再帰のために頭を悩ませる必要がない
  • 簡潔性: 複数回のループが必要だったものを1単語で実現できる
  • 安全性: 明示的な入出力 (関数の引数と結果) は静的型チェックの対象であり, コーディングの誤りの大多数は型エラーとしてキャッチされる。
  • 高速: コレクション演算は最適化されている。逐次コレクションは par メソッドで並列コレクションに変換できる。
  • 統一性: 意味のある限りどの型に対しても同じ演算を提供している

Scalaコレクションは追加・削除・更新を許す scale.collection.mutable と 不変である scale.collection.immutable の2つに大まかに分類される。
デフォルトでは Scala はイミュータブルなコレクションを選択する。デフォルトでミュータブルなコレクションを使うには, 明示的に collection.mutable.Set のように書く必要がある。

Traversableトレイト

コレクション階層の最上位は Traversableトレイトで抽象メソッドは, コレクションの全ての要素を辿り各要素に演算fを適用する foreachメソッドのみ。

def foreach[U] (f: Elem => U)

f の結果は U なので全て破棄される。Println のように副作用だけ行われる。

トレイトとは特徴、特性、形質などを意味し, コード再利用の基本単位となる。メソッドとフィールドの定義をカプセル化したもので, クラスにミックスインすることにより再利用でき, 低機能なシンインターフェースから高機能なリッチインターフェースへ変更する際に使われる。

Iterableトレイト

Iterableトレイトは抽象メソッド iterator を持っている。

Traversableトレイトから継承された抽象メソッド foreach は iteratorメソッドを使って実装されている。iterrator が next を持っている限り, それを呼び出す。
Iterableトレイトだけでなくより抽象度の高いTraversableトレイトがある理由の一つは, 効率的な実装を行うための自由度を持たせるためであるらしい。

Iterableのサブカテゴリには, Seq, Set, Map の3つのトレイトがある。

Seqトレイト

シーケンスを表す。シーケンスは長さがあり, 要素0から開始する添字を持っている Iterable の一種。シーケンスの lengthメソッドは, コレクション全般がもつ sizeメソッドの別名。

Seqトレイトには LinearSeq と IndexedSeq というサブトレイトがある。
この2つはパフォーマンス特性が異なり, LinearSeq は head, tail 演算が効率的なのに対して IndexedSeq は apply, length 演算が効率的である。
LinearSeqの代表例は List, Stream, IndexedSeqの代表例は Array や ArrayBuffer である。

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

scala> s.head
res1: Int = 1

scala> s.tail
res2: Seq[Int] = List(2, 3, 4, 5)

scala> s.size
res3: Int = 5

scala> s.length
res4: Int = 5

scala> s lengthCompare(5)
res5: Int = 0

scala> s indexOf(3)
res6: Int = 2

scala> s.apply(2)
res7: Int = 3

+: 演算子は先頭に追加, :+ 演算子は末尾に追加し新たな Seq を返す。

scala> 0 +: s
res8: Seq[Int] = List(0, 1, 2, 3, 4, 5)

scala> s :+ Seq(6)
res9: Seq[Any] = List(1, 2, 3, 4, 5, List(6))

scala> s :+ 6
res10: Seq[Int] = List(1, 2, 3, 4, 5, 6)

scala> val a = Seq("a", "d", "e", "b", "c")
a: Seq[String] = List(a, d, e, b, c)

sortedメソッドは要素をソートした Seq を返す。reverseは要素を逆順にした Seq を返す。

scala> a.sorted
res11: Seq[String] = List(a, b, c, d, e)

scala> a.sorted.reverse
res12: Seq[String] = List(e, d, c, b, a)

Setトレイト

重複する要素を持たない Iterable で, 和集合・積集合・差集合など集合演算をもつ。
containsメソッドは引数の要素が集合に含まれているかを Boolean で返す。applyメソッド, set(elem) も同様。

scala> val fruit = Set("apple", "orange", "peach", "banana")
fruit: scala.collection.immutable.Set[String] = Set(apple, orange, peach, banana)

scala> fruit contains "peach"
res1: Boolean = true

scala> fruit contains "potato"
res2: Boolean = false

scala> fruit apply "peach"
res3: Boolean = true

scala> fruit apply "potato"
res4: Boolean = false

集合演算には記号と英字の2種類がある。

  • 積集合: & または intersect
  • 和集合: | または union
  • 差集合: &~ または diff
scala> val fruit2 = Set("apple", "orange", "potato")
fruit2: scala.collection.immutable.Set[String] = Set(apple, orange, potato)

scala> fruit diff fruit2
res5: scala.collection.immutable.Set[String] = Set(peach, banana)

scala> fruit &~ fruit2
res6: scala.collection.immutable.Set[String] = Set(peach, banana)

scala> fruit intersect fruit2
res7: scala.collection.immutable.Set[String] = Set(apple, orange)

scala> fruit & fruit2
res8: scala.collection.immutable.Set[String] = Set(apple, orange)

scala> fruit union fruit2
res9: scala.collection.immutable.Set[String] = Set(peach, banana, potato, orange, apple)

scala> fruit | fruit2
res10: scala.collection.immutable.Set[String] = Set(peach, banana, potato, orange, apple)

ミュータブルな Set を使う場合, 明示的に指定する。

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

scala> s += 4
res11: s.type = Set(1, 2, 3, 4)

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

scala> s.add(5)
res13: Boolean = true

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

scala> s.remove(1)
res15: Boolean = true

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

集合のサイズが小さい場合はイミュータブルの方が効率的である。

Mapトレイト

Map は key と value がペアになった Iterable。代表的な get の定義は以下。

def get (key): Option[Value]

key に対応する value が格納されていれば, Someに包んで返す。key が定義されていない場合は None を返す。

scala> val m = Map("x" -> 24, "y" -> 25, "z" -> 26)
m: scala.collection.immutable.Map[String,Int] = Map(x -> 24, y -> 25, z -> 26)

scala> m get "x"
res1: Option[Int] = Some(24)

scala> m get "n"
res2: Option[Int] = None

apply は key に対応する value があればそのままの値を返す。ただし key が定義されていない場合は例外が発生する。

scala> m apply "x"
res3: Int = 24

scala> m apply "n"
java.util.NoSuchElementException: key not found: n
  at scala.collection.immutable.Map$Map3.apply(Map.scala:156)
  ... 27 elided

getOrElse は失敗時のデフォルト値を指定できる。

scala> m getOrElse ("x", 1)
res4: Int = 24

scala> m getOrElse ("n", 1)
res5: Int = 1

scala> m("x")
res6: Int = 24

Mapのサブコレクションを取得するメソッド。

scala> m.keys
res7: Iterable[String] = Set(x, y, z)

scala> m.keySet
res8: scala.collection.immutable.Set[String] = Set(x, y, z)

scala> m.keysIterator
res9: Iterator[String] = non-empty iterator

scala> m.values
res10: Iterable[Int] = MapLike.DefaultValuesIterable(24, 25, 26)

scala> m.valuesIterator
res11: Iterator[Int] = non-empty iterator

Map の変形は Map に含まれているペアに対して filtering や 変形を行い新しい Map を返す。

scala> m.filterKeys(_ == "y")
res12: scala.collection.immutable.Map[String,Int] = Map(y -> 25)

scala> m.mapValues(_ * 2)
res13: scala.collection.immutable.Map[String,Int] = Map(x -> 48, y -> 50, z -> 52)


[1] 第8章:Scalaのコレクション(Seq, Set, Map)入門
[2] コレクションライブラリ(immutableとmutable)
[3] flatMapをマスターする