Open41

【Scala】コレクションまとめ 下書き

nanasinanasi

概要

  • コレクションクラスはたくさんあり、いい感じに体系化されている
  • それぞれのコレクションクラスには可変と不変がある
    • 基本は不変を使おう
    • 例外: Buffer(可変のみ)
  • 基底、可変、不変でパッケージが分けられている
nanasinanasi

可変が必要ならmutableだけをインポートして接頭辞をつけることが推奨されている。

import scala.collection.mutable

これにより、接頭辞なしの Set は不変なコレクションを参照するのに対し、mutable.Set は可変版を参照する。

nanasinanasi

Traversableトレイトは走査可能。

  • 抽象メソッドであるforeachが全ての演算の元になっている
  • インスタンス化できない...(Iteratorになる)
nanasinanasi

メソッドは基本的に以下の性質を持つ。

  • 非破壊的(可変コレクションを除く)
  • 新しい配列を返す or 副作用を実行する
nanasinanasi

Traversableで定義されているメソッドはたくさんある。
正直これだけでJavaScriptのArrayのメソッドの量超えてる(体感)。

nanasinanasi

++: Traversable同士を繋げる
なお、TraversableIteratorを繋げることもできる。
もちろん非破壊的で、メソッドは新しい配列を返す。

nanasinanasi

collect: filtermapを組み合わせたみたいなやつ

  • 引数に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を使うことはできる。
ただし、その場合どのケースにもマッチしない要素があると実行時エラーとなるので注意。

nanasinanasi
  • 変換: toMap, toListなど
  • コピー: BufferArrayにコピーする
  • サイズ: isEmptysizeなど
nanasinanasi

要素取得:

  • headOption: 最初の要素(オプション)
  • lastOption: 最後の要素(オプション)
  • find: JSにもあるやつ
nanasinanasi

サブコレクション:

  • filter, slice
  • tail: headを除いた要素, init: lastを除いた要素
  • take: n個の要素, drop: takeを除いた要素
nanasinanasi

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文使うとかありえなかったのに...
パターンマッチング、おそるべし)

nanasinanasi
  • 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))
nanasinanasi

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
}
nanasinanasi

使用例

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さん)
)
nanasinanasi

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を使っているのも、本来の用途じゃない感ある。

nanasinanasi

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の案)
でも管理する名前が多くて頭がこんがらがるし、↑自体が直感的にわかりにくいしで、一番最初に書いたやつの方が良さそう。

nanasinanasi

Iterable: 反復可能
このトレイトの機能は、抽象メソッドであるiteratorに基づいて実装されている。

nanasinanasi

これにはforeachメソッドのデフォルト実装がある。
しかし、多くのサブクラスは効率を上げるためこの実装をオーバーライドしている。

nanasinanasi

メソッド:

  • grouped, sliding: イテレーター
  • zip演算
  • sameElement: 比較演算
  • etc...
nanasinanasi

Seq

列 (Seq) トレイトは、長さ (length) があり、それぞれの要素に 0 から数えられた固定された添字 (index) がある Iterable の一種だ。

nanasinanasi

methods:

  • apply: 添字(Seq(1, 3, 4)(1)3)
  • length: sizeの別名
  • indexOf
  • :+: 最後に要素を追加
  • +:: 最初に要素を追加(要素 +: Seqなので注意)
  • updated: 添字で要素を置き換え
    • xs(i) = x: 可変列のみで使用可能
  • sorted: ソート
    • sortBy: ソートに使うキーを指定
    • sortWith: 比較に使う関数を指定
    • sortOrderingを取ることができる
nanasinanasi

ちなみに、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!")
})
nanasinanasi
  • reverse, etc
  • contains: JSのinclude
  • startsWith, endsWith, containsSlice: 比較
  • 集合演算各種
nanasinanasi
  • IndexSeq: head, tailが効率的
  • LinearSeq: 添字アクセス(apply), length, updatedが効率的

これらは新しい演算を定義しない。

nanasinanasi

よく使われる添字付き列の例としては scala.Array と scala.collection.mutable.ArrayBuffer がある。
Vector は添字付き列と線形列の間の興味深い折衷案だ

nanasinanasi

Bufferは可変列に分類される。
詳しい説明は略。

nanasinanasi

Set

集合 (Set) は要素の重複の無い Iterable だ。集合一般に定義される演算は次の表にまとめてあり、可変集合に関してはその次の表も見てほしい。これらの演算は以下のカテゴリーに分類される:

nanasinanasi
  • 加算である + と ++ は、単一もしくは複数の要素を集合に追加し、新たな集合を返す。
  • 減算である - と -- は、単一もしくは複数の要素を集合から削除し、新たな集合を返す。

Set が Traversable から継承する ++ は union と | の更なる別名だと考えることができるが、++ は Traversable の引数を取るが、union と | は集合を取る。

和集合と要素の追加はやってること同じなのか

nanasinanasi

もしかして、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")
nanasinanasi

いや、別にコンパニオンである必要はないのか
何でもいいからファクトリメソッドがあって、それが実質的なコンストラクタみたいな

言語仕様レベルでこうすることで、オブジェクトの生成をオブジェクト本体から切り離せる、みたいなメリットがあるのかな
もしかしたら、今までコンストラクタを使うことで無意識にオブジェクトの生成をオブジェクト自身に任せていたのかもしれない

nanasinanasi

よく考えたらTypeScriptのこの記法、便利だったな
やってることはこれと同じなのか

class User {
  constructor(
    public readonly name: string,
    public readonly id: string
  ) {}
}
nanasinanasi

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)
nanasinanasi

これ多分Streamにオブジェクトの生成の仕方だけ教えておいて(fib)、実際にどの範囲のオブジェクトを生成するかはクライアントから決められるようになってる

この例だとtakeWhileを使うことで要素を1000未満に絞っている
takeWhileStreamの範囲を絞るのに相性いい説

xs 内の要素を最初から次々とみて、条件関数 p を満たす限りつないでいったコレクション。

Hidden comment
Hidden comment
Hidden comment
Hidden comment
Hidden comment