【Scala】コレクションまとめ 下書き
Scalaのコレクションについてまとめた記事を書きたい。
このスクラップはその下書き。
参考:
概要
- コレクションクラスはたくさんあり、いい感じに体系化されている
- それぞれのコレクションクラスには可変と不変がある
- 基本は不変を使おう
- 例外:
Buffer
(可変のみ)
- 基底、可変、不変でパッケージが分けられている
可変が必要ならmutable
だけをインポートして接頭辞をつけることが推奨されている。
import scala.collection.mutable
これにより、接頭辞なしの Set は不変なコレクションを参照するのに対し、mutable.Set は可変版を参照する。
Traversable
トレイトは走査可能。
- 抽象メソッドである
foreach
が全ての演算の元になっている - インスタンス化できない...(
Iterator
になる)
メソッドは基本的に以下の性質を持つ。
- 非破壊的(可変コレクションを除く)
- 新しい配列を返す or 副作用を実行する
Traversable
で定義されているメソッドはたくさんある。
正直これだけでJavaScriptのArray
のメソッドの量超えてる(体感)。
++
: Traversable
同士を繋げる
なお、Traversable
とIterator
を繋げることもできる。
もちろん非破壊的で、メソッドは新しい配列を返す。
map
, flatMap
: JavaScriptにもあるやつ。
collect
: filter
とmap
を組み合わせたみたいなやつ
- 引数に
PartialFunction
を取る - ↑では
case
が使える
val list = List(1, 4, 5)
val result = list collect { // PartialFunction
case item if item % 2 == 0 => item / 2
} // List(2)
なお、PartialFunction
ではない普通の関数でもcase
を使うことはできる。
ただし、その場合どのケースにもマッチしない要素があると実行時エラーとなるので注意。
- 変換:
toMap
,toList
など - コピー:
Buffer
やArray
にコピーする - サイズ:
isEmpty
やsize
など
要素取得:
-
headOption
: 最初の要素(オプション) -
lastOption
: 最後の要素(オプション) -
find
: JSにもあるやつ
サブコレクション:
-
filter
,slice
-
tail
:head
を除いた要素,init
:last
を除いた要素 -
take
: n個の要素,drop
:take
を除いた要素
span
: (takeWhile, dropWhile)
例えば、要素が奇数のうちは最初のリストに入れて、そうでないものがあったらそれ以降は次のリストに入れる例。
パターンマッチングで実装してみたけど、普通にif
でもよかったかも
val list1 = List(1, 5, 3, 4, 5)
val r = list1 span {
case n if n % 2 != 0 => true
case _ => false
}
println(r) // (List(1, 5, 3),List(4, 5))
普通にやったら1行でよかった
val r = list1 span { n => n % 2 != 0 }
(JavaScriptならこんな無駄にswitch
文使うとかありえなかったのに...
パターンマッチング、おそるべし)
-
partition
: 条件を満たすものと満たさないもの -
splitAt
:n
の位置で2分する -
groupBy
: コレクションをMap
に変換する
groupBy
、けっこう便利かもしれない
パターンマッチングと合わせたら強そう
val list1 = List(1, 5, 3, 4, 5)
val r = list1 groupBy {
case n if n % 2 == 0 => "偶数"
case _ => "奇数"
} // HashMap(偶数 -> List(4), 奇数 -> List(1, 5, 3, 5))
Mapのkey-valuesをvalue-keysに変換するメソッド
def convert[K, V](dataMap: Map[K, List[V]]): Map[V, List[K]] = {
dataMap.values.flatten.toList.distinct // to values
.map(value => // to value -> keys
value ->
dataMap.keys.filter { key => dataMap(key).contains(value) }.toList
)
.toMap
}
使用例
val data = Map(
"Aさん" -> List("演目1", "演目4", "演目3"),
"Bさん" -> List("演目3", "演目2"),
"Cさん" -> List("演目4", "演目1", "演目5"),
"Dさん" -> List("演目2", "演目4")
)
convert(data)
// ↓
HashMap(
演目5 -> List(Cさん),
演目2 -> List(Bさん, Dさん),
演目1 -> List(Aさん, Cさん),
演目3 -> List(Aさん, Bさん),
演目4 -> List(Aさん, Cさん, Dさん)
)
TypeScript:
function convert<K, V>(dataMap: Map<K, V[]>): Map<V, K[]> {
const values = new Set([...dataMap.values()].flat())
const array = [...values].map(value => [ // to value -> keys
value,
[...dataMap.keys()].filter(key => dataMap.get(key)!.includes(value))
] as const)
return new Map(array)
}
new Map
の中に全ての処理を書いているのが見づらすぎたため、複数行に分けている。
全体的に[...]
が多すぎて見づらい印象。
あと、値をユニークにするためにSet
を使っているのも、本来の用途じゃない感ある。
TypeScript(昔作ったやつ):
function convert<K, V>(dataMap: Map<K, V[]>): Map<V, K[]> {
return [...dataMap].reduce((result, current) => {
const [key, values] = current;
for (const value of values) {
if(result.has(value)) {
result.get(value)!.push(key);
} else {
result.set(value, [key]);
}
}
return result;
}, new Map<V, K[]>());
}
あまりreduce
の中に全てを詰め込んではいけない気がしてきた。
これをScalaにした結果:
def convert[K, V](dataMap: Map[K, List[V]]): Map[V, List[K]] = {
dataMap.foldLeft(Map[V, List[K]]()) {
case (result, (key, values)) => {
values.foldLeft(result) { (acc, value) =>
acc + (value -> (acc.getOrElse(value, List()) :+ key))
}
}
}
}
foldLeft
を2重にするとこんなにすっきりするのか...(ChatGPTの案)
でも管理する名前が多くて頭がこんがらがるし、↑自体が直感的にわかりにくいしで、一番最初に書いたやつの方が良さそう。
Iterable
: 反復可能
このトレイトの機能は、抽象メソッドであるiterator
に基づいて実装されている。
Seq
列 (Seq) トレイトは、長さ (length) があり、それぞれの要素に 0 から数えられた固定された添字 (index) がある Iterable の一種だ。
methods:
-
apply
: 添字(Seq(1, 3, 4)(1)
は3
) -
length
:size
の別名 indexOf
-
:+
: 最後に要素を追加 -
+:
: 最初に要素を追加(要素 +: Seq
なので注意) -
updated
: 添字で要素を置き換え-
xs(i) = x
: 可変列のみで使用可能
-
-
sorted
: ソート-
sortBy
: ソートに使うキーを指定 -
sortWith
: 比較に使う関数を指定 -
sort
はOrdering
を取ることができる
-
ちなみに、PartialFuncion
じゃないところでもパターンマッチは使える。
ただし、以下のように条件に当てはまらないパターンがあると、scala.MatchError
が発生する。
val list = List(
(5, 1),
(7, 2),
(2, 3)
)
println(list.sortBy {
case (key, _) if key % 5 == 0 => key
// 一応こういう感じにできる
case _ => throw new Throwable("Error!")
})
-
reverse
, etc -
contains
: JSのinclude
-
startsWith
,endsWith
,containsSlice
: 比較 - 集合演算各種
-
IndexSeq
:head
,tail
が効率的 -
LinearSeq
: 添字アクセス(apply
),length
,updated
が効率的
これらは新しい演算を定義しない。
よく使われる添字付き列の例としては scala.Array と scala.collection.mutable.ArrayBuffer がある。
Vector は添字付き列と線形列の間の興味深い折衷案だ
Buffer
は可変列に分類される。
詳しい説明は略。
Set
集合 (Set) は要素の重複の無い Iterable だ。集合一般に定義される演算は次の表にまとめてあり、可変集合に関してはその次の表も見てほしい。これらの演算は以下のカテゴリーに分類される:
もしかして、Scalaにおけるコンストラクタってコンパニオンオブジェクトのapply
メソッド...?
case class User(name: String, id: String)
object User {
// コンストラクタ
def apply(name: String): User = {
import java.util.UUID.randomUUID
User(name, randomUUID().toString())
}
}
val user = User("nanasi")
いや、別にコンパニオンである必要はないのか
何でもいいからファクトリメソッドがあって、それが実質的なコンストラクタみたいな
言語仕様レベルでこうすることで、オブジェクトの生成をオブジェクト本体から切り離せる、みたいなメリットがあるのかな
もしかしたら、今までコンストラクタを使うことで無意識にオブジェクトの生成をオブジェクト自身に任せていたのかもしれない
よく考えたらTypeScriptのこの記法、便利だったな
やってることはこれと同じなのか
class User {
constructor(
public readonly name: string,
public readonly id: string
) {}
}
Stream
: 遅延評価
これ結構面白いことできそう
以下はドキュメントを参考に作ったフィボナッチ数列
def fib(a: Int, b: Int): Stream[Int] = a #:: fib(b, b + a)
val result = fib(1, 3).takeWhile { num => num < 1000 }.toList
println(result) // List(1, 3, 4, 7, 11, 18, 29, 47, 76, 123, 199, 322, 521, 843)
これ多分Stream
にオブジェクトの生成の仕方だけ教えておいて(fib
)、実際にどの範囲のオブジェクトを生成するかはクライアントから決められるようになってる
この例だとtakeWhile
を使うことで要素を1000
未満に絞っている
takeWhile
はStream
の範囲を絞るのに相性いい説
xs 内の要素を最初から次々とみて、条件関数 p を満たす限りつないでいったコレクション。