【Scala】Seqの全て!定義から使い方、Listとの比較まで解説

はい、承知いたしました。ScalaのSeqについて、定義から使い方、Listとの比較までを網羅的に解説する記事を作成します。約5000語を目指し、詳細な説明を含めます。


【Scala】Seqの全て!定義から使い方、Listとの比較まで徹底解説

はじめに

Scalaは、関数型プログラミングとオブジェクト指向プログラミングを統合した強力な言語です。その魅力の一つに、洗練されたコレクションライブラリがあります。Scalaのコレクションライブラリは、イミュータブル(不変)性とミュータブル(可変)性の両方をサポートしつつ、豊富な高階関数を提供することで、データの操作を安全かつ簡潔に行うことを可能にしています。

コレクションライブラリの中でも、最も頻繁に利用されるデータ構造の一つが「シーケンス(Sequence)」、ScalaではSeqトレイトによって表現されるものです。Seqは「順序付けられた要素の集まり」を抽象化したものであり、プログラミングにおけるリスト、配列、ベクトルなど、多くの基本的なデータ構造の基盤となっています。

しかし、一口にSeqと言っても、その実態である具体的なクラス(List, Vector, ArraySeq, ArrayBufferなど)は内部構造やパフォーマンス特性が大きく異なります。これらの違いを理解せずに利用すると、意図しないパフォーマンス劣化を招く可能性もあります。

この記事では、ScalaのSeqトレイトについて、その定義から始め、主要なメソッド、具体的な実装クラス(特にListVector)、そしてイミュータブル/ミュータブルなSeqの使い分けまでを徹底的に解説します。この記事を読むことで、あなたはScalaのSeqを自在に操り、より効率的かつ安全なコードを書けるようになるでしょう。

さあ、ScalaのSeqの世界へ飛び込みましょう!

Scalaコレクションライブラリの階層におけるSeqの位置づけ

Scalaのコレクションライブラリは、トレイトの階層構造によって設計されています。この階層を理解することは、各コレクション型の特性や利用可能なメソッドを把握する上で非常に重要です。

最上位には、ほとんどのコレクションが継承するIterableトレイトがあります。Iterableは、要素を順番に辿ることができる(イテレーションできる)コレクションを表します。foreach, map, filter, foldLeftなど、多くの高階関数はIterableトレイトで定義されており、Iterableを継承するすべてのコレクションで利用できます。

Iterableの直下には、主要な3つのトレイトがあります。

  1. Seq (Sequence): 順序付けられた要素の集まり。要素にはインデックスでアクセスできます。
  2. Set: 重複しない要素の集まり。要素の順序は保証されません(ただし、SortedSetなどの実装を除く)。
  3. Map: キーと値のペアの集まり。キーは重複しません。

この記事の主役であるSeqは、この中で「順序付けられている」という特徴を持つコレクションを抽象化しています。つまり、SeqIterableの性質(イテレーション可能)に加え、要素が特定の順番で並んでおり、その順番(インデックス)を指定して要素を取り出したり、並び替えたりといった操作が可能であることを保証します。

scala
// コレクション階層のイメージ (簡易版)
// Iterable
// ├── Seq (順序あり, インデックスアクセス可)
// ├── Set (順序なし, 重複なし)
// └── Map (キーと値のペア, キー重複なし)

Seqトレイトを継承する具体的なクラスには、イミュータブルなものとミュータブルなものが存在します。

  • イミュータブル Seq: scala.collection.immutable パッケージにあります。代表的なものに List, Vector, ArraySeq, LazyList (旧 Stream) があります。これらのコレクションに対する操作(要素の追加や削除など)は、元のコレクションを変更せず、新しいコレクションを生成して返します。一度作成したら内容が変わらないため、並行処理において安全に扱えます。
  • ミュータブル Seq: scala.collection.mutable パッケージにあります。代表的なものに ArrayBuffer, ListBuffer, Queue, Stack があります。これらのコレクションは、in-placeで(元のオブジェクト自身を)変更する操作を提供します。効率が良い場合もありますが、複数の箇所から同時にアクセス・変更される際には注意が必要です。

Scalaの標準ライブラリでは、特に指定がない限り、Seqはイミュータブルなscala.collection.immutable.Seqを指すことが一般的です。これは、Scalaがイミュータビリティを推奨する思想に基づいているためです。

Seqトレイトの詳細

定義と特徴

Seq[A]トレイトは、型パラメータAを持つ、順序付けられた要素の集まりを表します。その最も基本的な特徴は以下の2点です。

  1. 順序付けられている (Ordered): 要素が特定の順番で並んでいます。要素の追加順序などがその順番を決定します。
  2. インデックスアクセスが可能 (Indexed Access): 0から始まる整数インデックスを使って、コレクション内の任意の要素に直接アクセスできます。

この「インデックスアクセスが可能」という特徴は、Seqが提供する最も重要な機能の一つです。Scalaでは、このインデックスアクセスはapplyメソッドとして定義されています。

“`scala
trait Seq[+A] extends Iterable[A] with PartialFunction[Int, A] {
// … 多くのメソッド定義 …

/*
* Selects an element by its index.
/
def apply(i: Int): A

// … 他のメソッド …
}
“`

apply(i: Int): Aメソッドは、指定されたインデックスiにある要素を返します。Scalaでは、オブジェクトに対してobj(args)のように呼び出すと、コンパイラが自動的にobj.apply(args)に変換するため、seq.apply(0)のように書かなくても、seq(0)のように簡潔に要素にアクセスできます。これは、配列のarray[0]のような構文に似ていますが、配列だけでなくSeq全般で利用できる Scalaらしい糖衣構文です。

applyメソッドに関する補足:

  • 指定したインデックスがコレクションの有効な範囲外(0未満またはlength以上)の場合、applyメソッドはIndexOutOfBoundsExceptionをスローします。
  • SeqトレイトはPartialFunction[Int, A]も継承しています。これは、apply(Int)メソッドを持つこと、そしてisDefinedAt(Int)メソッド(指定されたインデックスが有効かどうかを判定)を持つことを意味します。しかし、通常はapplyメソッドを直接利用することがほとんどです。

イミュータブルなSeqの主な実装

scala.collection.immutable.Seqを継承する代表的なクラスをいくつか紹介します。これらのクラスは、それぞれ異なる内部実装を持ち、得意な操作やパフォーマンス特性が異なります。

  • List[A]:

    • 単方向連結リストとして実装されています。
    • 要素は head(先頭要素)と tail(残りのリスト)のペアで構成されます。
    • 先頭への要素の追加(::または+:)や、先頭要素・残りのリストへのアクセス(head, tail)は非常に効率的です(O(1))。
    • 一方、末尾への要素の追加(:+")や、中間・末尾の要素へのアクセス(インデックスアクセス)は、リストを先頭から辿る必要があるため、要素数に比例して時間がかかります(O(n))。
    • イミュータブルなので、操作は新しいリストを生成します。
    • 再帰的な処理と相性が良いです。
  • Vector[A]:

    • バランスの取れた木構造(一般的には32-wayフィンガーツリー)として実装されています。
    • 要素へのランダムアクセス、末尾への追加、中間での挿入や削除を伴う「更新」(実際には新しいVectorの生成)操作が効率的です(O(log n) または償却O(1))。
    • 特に、イミュータブルでありながら高速なランダムアクセスと更新が必要な場合に適しています。
    • 要素数が多くなってもパフォーマンスの劣化が緩やかです。
  • ArraySeq[A]:

    • 内部的にJava配列を使用しています。
    • 要素へのランダムアクセスとミュータブルな更新は非常に高速です(O(1))。
    • ただし、イミュータブルなArraySeqに対する要素の追加や削除、中間での挿入などの操作は、内部的に新しい配列を生成する必要があるため、コストが高くなります(O(n))。
    • 主に既存のJava配列をScalaのコレクションとして扱いたい場合や、サイズ固定のSeqとして利用する場合に便利です。
  • LazyList[A] (旧 Stream[A]):

    • 要素が遅延評価されるSeqです。
    • 要素が必要とされるまで計算が遅延されるため、無限シーケンスを表現したり、非常に大きなシーケンスを効率的に扱ったりするのに適しています。
    • 要素は一度計算されるとメモ化されます(次回アクセス時には再計算されない)。
    • ただし、メモ化によりメモリを消費する可能性があるため、注意が必要です。

ミュータブルなSeqの主な実装

scala.collection.mutable.Seqを継承する代表的なクラスです。

  • ArrayBuffer[A]:

    • 可変長の配列(バッファ)として実装されています。
    • 内部的に配列を持ち、容量が足りなくなるとより大きな新しい配列に要素をコピーして拡張します。
    • 末尾への要素の追加は効率的です(償却O(1))。
    • 要素へのランダムアクセスやミュータブルな更新も効率的です(O(1))。
    • 中間での要素の挿入や削除は、後続の要素を移動させる必要があるため、コストが高くなります(O(n))。
    • ミュータブルなSeqの中で最もよく使われます。
  • ListBuffer[A]:

    • 連結リストを効率的に構築するためのミュータブルなバッファです。
    • 要素を効率的に追加し、最後に不変なListに変換する場合に利用します。
    • 末尾への要素の追加は効率的です(O(1))。
    • ただし、Listと同様に、インデックスアクセスや中間要素の操作は効率が良くありません。

他にもQueue[A], Stack[A]などがありますが、これらは特定の操作パターン(FIFO, LIFO)に特化したSeqの実装と言えます。

Seqの基本的な使い方

ここでは、Seqを生成し、要素にアクセスし、操作する基本的な方法を紹介します。主にイミュータブルなSeq(特にListVector)を想定して説明します。

生成

Seqを生成する最も一般的な方法はいくつかあります。

  1. コンパニオンオブジェクトの apply メソッド:
    これは非常に簡潔で、最もよく使われる方法です。デフォルトではイミュータブルなscala.collection.immutable.Seq(通常はListまたはVector)が生成されます。要素の型は推論されます。
    scala
    val seq1: Seq[Int] = Seq(10, 20, 30, 40, 50) // 推論により Seq[Int]
    val seq2: Seq[String] = Seq("apple", "banana", "cherry") // 推論により Seq[String]
    val emptySeq: Seq[Double] = Seq.empty[Double] // 空のSeqを明示的に生成

    Seq.empty[A]は、指定された型の空のシーケンスを生成する効率的な方法です。

  2. 特定の型(List, Vectorなど)のコンパニオンオブジェクトを使う:
    生成したい具体的なSeqの実装クラスが分かっている場合は、そのクラスのコンパニオンオブジェクトを使います。
    scala
    val list1: List[Int] = List(1, 2, 3)
    val vector1: Vector[String] = Vector("A", "B", "C")
    val arraySeq1: ArraySeq[Boolean] = ArraySeq(true, false, true)

  3. Range:
    整数シーケンスを簡単に生成できます。toは終端を含む、untilは終端を含みません。byでステップを指定できます。
    scala
    val range1: Seq[Int] = 1 to 5 // List(1, 2, 3, 4, 5)
    val range2: Seq[Int] = 1 until 5 // List(1, 2, 3, 4)
    val range3: Seq[Int] = 0 to 10 by 2 // List(0, 2, 4, 6, 8, 10)

    RangeはIndexedSeqを実装しており、効率的なインデックスアクセスが可能です。

  4. filltabulate:
    同じ要素を繰り返したり、インデックスに基づいて要素を生成したりするのに便利です。
    scala
    val fillSeq: Seq[String] = Seq.fill(3)("Hi") // List("Hi", "Hi", "Hi")
    val tabulateSeq: Seq[Int] = Seq.tabulate(5)(_ * 2) // List(0, 2, 4, 6, 8) // i => i * 2

    fill(n)(elem)は、要素elemn回繰り返すSeqを生成します。
    tabulate(n)(f)は、インデックスi (0からn-1まで) を関数fに適用して要素を生成するSeqを生成します。

要素へのアクセス

Seqは順序付けられているため、インデックスや位置に基づいて要素にアクセスできます。

  1. インデックスアクセス (apply):
    最も直接的なアクセス方法です。seq(index)のように記述します。
    scala
    val mySeq: Seq[Int] = Seq(10, 20, 30, 40, 50)
    println(mySeq(0)) // 10
    println(mySeq(2)) // 30
    // println(mySeq(5)) // IndexOutOfBoundsException が発生

  2. head, tail, init, last:
    リストの最初や最後の要素、あるいはそれ以外の部分を取得するメソッドです。
    “`scala
    val mySeq: Seq[Int] = Seq(10, 20, 30, 40, 50)
    println(mySeq.head) // 10 (最初の要素)
    println(mySeq.tail) // List(20, 30, 40, 50) (最初の要素以外のSeq)
    println(mySeq.last) // 50 (最後の要素)
    println(mySeq.init) // List(10, 20, 30, 40) (最後の要素以外のSeq)

    // 空のSeqに対してこれらのメソッドを呼び出すと UnsupportedOperationException が発生
    // val emptySeq: Seq[Int] = Seq.empty
    // emptySeq.head // Exception!
    “`

  3. headOption, lastOption:
    空のSeqでも例外を投げずに安全に最初の要素や最後の要素を取得したい場合は、Optionを返すこれらのメソッドを使います。
    “`scala
    val mySeq: Seq[Int] = Seq(10, 20, 30, 40, 50)
    println(mySeq.headOption) // Some(10)
    println(mySeq.lastOption) // Some(50)

    val emptySeq: Seq[Int] = Seq.empty
    println(emptySeq.headOption) // None
    println(emptySeq.lastOption) // None
    “`

要素の追加・削除

イミュータブルなSeqの場合、要素の追加や削除は元のSeqを変更せず、新しいSeqを生成して返します。ミュータブルなSeqの場合は、元のSeq自身を変更します。

イミュータブルなSeqの操作:

  • 要素の追加:

    • +: (または ::): 要素を先頭に追加します。リストなどの連結リスト実装では非常に効率的です。
    • :+: 要素を末尾に追加します。リストでは非効率的ですが、Vectorでは比較的効率的です。
      “`scala
      val baseSeq: Seq[Int] = Seq(20, 30)
      val newSeq1 = 10 +: baseSeq // Seq(10, 20, 30)
      val newSeq2 = baseSeq :+ 40 // Seq(20, 30, 40)

    val listBase = List(20, 30)
    val listNew1 = 10 :: listBase // List(10, 20, 30) (::+:と同じ意味)
    val listNew2 = listBase :+ 40 // List(20, 30, 40) (Listの場合はO(n))

    val vectorBase = Vector(20, 30)
    val vectorNew1 = 10 +: vectorBase // Vector(10, 20, 30) (Vectorの場合は効率的)
    val vectorNew2 = vectorBase :+ 40 // Vector(20, 30, 40) (Vectorの場合は効率的)
    “`

  • 他のSeqとの結合:

    • ++: 別のSeqの要素を結合します。
      scala
      val seqA = Seq(1, 2)
      val seqB = Seq(3, 4)
      val combinedSeq = seqA ++ seqB // Seq(1, 2, 3, 4)
  • 要素の削除:
    イミュータブルなSeqに直接要素を「削除」する汎用的なメソッドは、インデックス指定では提供されていません。インデックスで指定して要素を削除したい場合は、takedropを組み合わせて新しいSeqを作成するか、filterNotなどを使います。
    “`scala
    val mySeq = Seq(10, 20, 30, 40, 50)
    // インデックス 2 (値 30) を削除したい場合
    val seqWithout30 = mySeq.take(2) ++ mySeq.drop(3) // Seq(10, 20, 40, 50)

    // 値 30 を削除したい場合
    val seqWithoutValue30 = mySeq.filterNot(_ == 30) // Seq(10, 20, 40, 50)
    “`

ミュータブルなSeqの操作:

ミュータブルなSeq(例: ArrayBuffer)は、元のオブジェクトを変更するメソッドを提供します。

  • 要素の追加:

    • +=: 単一の要素を末尾に追加します。
    • ++=: 別のSeqの要素を末尾に追加します。
      “`scala
      import scala.collection.mutable.ArrayBuffer

    val buffer = ArrayBuffer(10, 20)
    buffer += 30 // ArrayBuffer(10, 20, 30)
    buffer ++= Seq(40, 50) // ArrayBuffer(10, 20, 30, 40, 50)
    buffer.append(60) // ArrayBuffer(10, 20, 30, 40, 50, 60) (appendも使える)
    buffer.prepend(0) // ArrayBuffer(0, 10, 20, 30, 40, 50, 60) (先頭への追加)
    buffer.insert(1, 5) // ArrayBuffer(0, 5, 10, 20, 30, 40, 50, 60) (中間への挿入)
    “`

  • 要素の削除:

    • -=: 指定した単一の要素を削除します(最初に見つかったもの)。
    • --=: 別のSeqに含まれる要素を全て削除します。
    • remove: インデックスを指定して要素を削除します。
    • trimStart, trimEnd: 先頭または末尾から指定した数の要素を削除します。
      “`scala
      import scala.collection.mutable.ArrayBuffer

    val buffer = ArrayBuffer(10, 20, 30, 20, 40)
    buffer -= 20 // ArrayBuffer(10, 30, 20, 40) (最初の20が削除される)
    buffer –= Seq(30, 40) // ArrayBuffer(10, 20)
    buffer.remove(1) // ArrayBuffer(10) (インデックス 1 の要素を削除)
    buffer.trimStart(1) // ArrayBuffer()
    “`

イテレーション

Seqの要素を順番に処理するには、forループやforeachメソッドを使います。

“`scala
val mySeq: Seq[String] = Seq(“apple”, “banana”, “cherry”)

// for ループ
for (item <- mySeq) {
println(s”Item: $item”)
}

// foreach メソッド
mySeq.foreach(item => println(s”Item: $item”))
// または短縮形
mySeq.foreach(println) // Item: apple, Item: banana, Item: cherry (改行区切り)

// インデックス付きでイテレーション
mySeq.zipWithIndex.foreach { case (item, index) =>
println(s”Item at index $index: $item”)
}
“`

Seqの主要メソッド

SeqIterableトレイトを継承しているため、Iterableで定義されている豊富な高階関数(map, filter, foldLeftなど)を全て利用できます。さらに、Seq独自のメソッドや、IndexedSeqなど下位トレイトで効率的に実装されているメソッドもあります。ここでは、よく利用される主要なメソッドをカテゴリー別に紹介します。

変換系

既存のSeqから新しいSeqを生成するメソッドです。

  • map[B](f: A => B): Seq[B]:
    各要素に関数fを適用し、結果を新しいSeqとして返します。要素の型を変えることができます。
    scala
    val numbers = Seq(1, 2, 3, 4)
    val doubled = numbers.map(_ * 2) // Seq(2, 4, 6, 8)
    val strings = numbers.map(_.toString) // Seq("1", "2", "3", "4")

  • flatMap[B](f: A => IterableOnce[B]): Seq[B]:
    各要素に関数fを適用し、その結果(コレクション)を平坦化して一つの新しいSeqとして返します。
    “`scala
    val words = Seq(“hello”, “world”)
    val chars = words.flatMap(_.toCharArray) // Seq(‘h’, ‘e’, ‘l’, ‘l’, ‘o’, ‘w’, ‘o’, ‘r’, ‘l’, ‘d’)

    val nested = Seq(Seq(1, 2), Seq(3, 4))
    val flat = nested.flatMap(identity) // Seq(1, 2, 3, 4)
    “`

  • collect[B](pf: PartialFunction[A, B]): Seq[B]:
    指定された偏関数pfが適用可能な要素のみを選択し、その結果を新しいSeqとして返します。filtermapを組み合わせたようなものです。
    scala
    val list = List(1, "two", 3, "four")
    val ints = list.collect { case i: Int => i } // List(1, 3)
    val strings = list.collect { case s: String => s.toUpperCase } // List("TWO", "FOUR")

  • filter(p: A => Boolean): Seq[A]:
    述語ptrueを返す要素のみを新しいSeqとして返します。
    scala
    val numbers = Seq(1, 2, 3, 4, 5, 6)
    val evens = numbers.filter(_ % 2 == 0) // Seq(2, 4, 6)

  • filterNot(p: A => Boolean): Seq[A]:
    述語pfalseを返す要素のみを新しいSeqとして返します。filter(p)の逆です。
    scala
    val numbers = Seq(1, 2, 3, 4, 5, 6)
    val odds = numbers.filterNot(_ % 2 == 0) // Seq(1, 3, 5)

  • take(n: Int): Seq[A]:
    先頭からn個の要素を新しいSeqとして返します。
    scala
    val seq = Seq(1, 2, 3, 4, 5)
    val firstThree = seq.take(3) // Seq(1, 2, 3)

  • drop(n: Int): Seq[A]:
    先頭からn個の要素を除いた残りを新しいSeqとして返します。
    scala
    val seq = Seq(1, 2, 3, 4, 5)
    val afterTwo = seq.drop(2) // Seq(3, 4, 5)

  • takeWhile(p: A => Boolean): Seq[A]:
    述語ptrueを返す間、先頭から要素を取り出し、最初のfalseになる要素の直前までを新しいSeqとして返します。
    scala
    val seq = Seq(1, 2, 3, 4, 5, 1)
    val prefix = seq.takeWhile(_ < 4) // Seq(1, 2, 3)

  • dropWhile(p: A => Boolean): Seq[A]:
    述語ptrueを返す間、先頭から要素を削除し、最初のfalseになる要素から末尾までを新しいSeqとして返します。
    scala
    val seq = Seq(1, 2, 3, 4, 5, 1)
    val suffix = seq.dropWhile(_ < 4) // Seq(4, 5, 1)

  • splitAt(n: Int): (Seq[A], Seq[A]):
    指定されたインデックスnSeqを分割し、2つのSeqのペアとして返します。take(n)drop(n)の結果をタプルで返します。
    scala
    val seq = Seq(1, 2, 3, 4, 5)
    val (prefix, suffix) = seq.splitAt(3) // (Seq(1, 2, 3), Seq(4, 5))

  • span(p: A => Boolean): (Seq[A], Seq[A]):
    takeWhile(p)dropWhile(p)の結果をタプルで返します。
    scala
    val seq = Seq(1, 2, 3, 4, 5, 1)
    val (truePrefix, falseSuffix) = seq.span(_ < 4) // (Seq(1, 2, 3), Seq(4, 5, 1))

  • distinct: Seq[A] / distinctBy[B](f: A => B): Seq[A]:
    重複する要素を取り除いた新しいSeqを返します。distinctByは、要素自身ではなく、各要素に関数fを適用した結果に基づいて重複を判定します。要素の元の順序は維持されます。
    “`scala
    val seq = Seq(1, 2, 1, 3, 2)
    val distinctSeq = seq.distinct // Seq(1, 2, 3)

    val people = Seq((“Alice”, 30), (“Bob”, 25), (“Charlie”, 30), (“Alice”, 35))
    val distinctByName = people.distinctBy(._1) // Seq((“Alice”, 30), (“Bob”, 25), (“Charlie”, 30))
    val distinctByAge = people.distinctBy(
    ._2) // Seq((“Alice”, 30), (“Bob”, 25))
    “`

  • sorted[B >: A](implicit ord: Ordering[B]): Seq[A]:
    要素を自然順序または暗黙的なOrderingに従ってソートした新しいSeqを返します。
    “`scala
    val numbers = Seq(3, 1, 4, 1, 5, 9, 2, 6)
    val sortedNumbers = numbers.sorted // Seq(1, 1, 2, 3, 4, 5, 6, 9)

    val strings = Seq(“banana”, “apple”, “cherry”)
    val sortedStrings = strings.sorted // Seq(“apple”, “banana”, “cherry”)
    “`

  • sortBy[B](f: A => B)(implicit ord: Ordering[B]): Seq[A]:
    各要素に関数fを適用した結果に基づいてソートした新しいSeqを返します。
    scala
    val people = Seq(("Alice", 30), ("Bob", 25), ("Charlie", 35))
    val sortedByAge = people.sortBy(_._2) // Seq(("Bob", 25), ("Alice", 30), ("Charlie", 35))
    val sortedByName = people.sortBy(_._1) // Seq(("Alice", 30), ("Bob", 25), ("Charlie", 35))

  • sortWith(lt: (A, A) => Boolean): Seq[A]:
    2つの要素を受け取る比較関数lt(最初の要素が2番目の要素より小さい場合にtrueを返す)を指定してソートした新しいSeqを返します。より柔軟なソートが可能です。
    scala
    val numbers = Seq(3, 1, 4, 1, 5)
    val sortedDescending = numbers.sortWith(_ > _) // Seq(5, 4, 3, 1, 1)

  • reverse: Seq[A]:
    要素の順番を逆にした新しいSeqを返します。
    scala
    val seq = Seq(1, 2, 3)
    val reversed = seq.reverse // Seq(3, 2, 1)

  • zip[B](that: IterableOnce[B]): Seq[(A, B)]:
    このSeqの要素と、別のIterableOnceの要素をペアにしたタプルの新しいSeqを返します。短い方に合わせて要素が切り詰められます。
    scala
    val numbers = Seq(1, 2, 3)
    val letters = Seq("a", "b", "c", "d")
    val zipped = numbers.zip(letters) // Seq((1, "a"), (2, "b"), (3, "c"))

  • zipWithIndex: Seq[(A, Int)]:
    各要素とそのインデックスをペアにしたタプルの新しいSeqを返します。
    scala
    val letters = Seq("a", "b", "c")
    val zippedWithIndex = letters.zipWithIndex // Seq(("a", 0), ("b", 1), ("c", 2))

畳み込み・集計系

Seqの要素をまとめて一つの結果を生成するメソッドです。

  • reduce[B >: A](op: (B, A) => B): B / reduceLeft[B >: A](op: (B, A) => B): B:
    最初の要素を開始値として、左から順に関数opを適用し、最終的な結果を返します。空のSeqに対して呼び出すと例外をスローします。
    scala
    val numbers = Seq(1, 2, 3, 4)
    val sum = numbers.reduce(_ + _) // 1 + 2 + 3 + 4 = 10
    val max = numbers.reduce(_ max _) // 4

  • reduceOption[B >: A](op: (B, A) => B): Option[B]:
    reduceと同様ですが、空のSeqの場合はNoneを返します。
    scala
    val emptySeq: Seq[Int] = Seq.empty
    println(emptySeq.reduceOption(_ + _)) // None

  • foldLeft[B](z: B)(op: (B, A) => B): B:
    初期値zから開始して、左から順に関数op(アキュムレーターと要素を受け取り、新しいアキュムレーターを返す)を適用し、最終的な結果を返します。reduceLeftと似ていますが、初期値を指定できるため空のSeqでも安全です。
    scala
    val numbers = Seq(1, 2, 3, 4)
    val sum = numbers.foldLeft(0)(_ + _) // 0 + 1 + 2 + 3 + 4 = 10
    val concatenatedString = numbers.foldLeft("")(_ + _.toString) // "" + "1" + "2" + "3" + "4" = "1234"

    foldRightもありますが、foldLeftの方が一般的に効率的です(特にListに対して)。

  • sum[B >: A](implicit num: Numeric[B]): B:
    要素の合計を計算します。Numeric型クラスが必要です。
    scala
    val numbers = Seq(1, 2, 3, 4)
    val total = numbers.sum // 10

  • product[B >: A](implicit num: Numeric[B]): B:
    要素の積を計算します。Numeric型クラスが必要です。
    scala
    val numbers = Seq(1, 2, 3, 4)
    val prod = numbers.product // 24

  • min[B >: A](implicit ord: Ordering[B]): A:
    要素の中で最小値を返します。空のSeqに対して呼び出すと例外をスローします。
    scala
    val numbers = Seq(3, 1, 4, 1, 5)
    val minimum = numbers.min // 1

  • max[B >: A](implicit ord: Ordering[B]): A:
    要素の中で最大値を返します。空のSeqに対して呼び出すと例外をスローします。
    scala
    val numbers = Seq(3, 1, 4, 1, 5)
    val maximum = numbers.max // 5

    minOption, maxOptionもあり、空のSeqでも安全です。

  • groupBy[K](f: A => K): Map[K, Seq[A]]:
    各要素に関数fを適用した結果をキーとして、要素をグループ化し、Mapとして返します。
    scala
    val fruits = Seq("apple", "banana", "cherry", "date")
    val groupedByLength = fruits.groupBy(_.length) // Map(5 -> Seq("apple"), 6 -> Seq("banana", "cherry"), 4 -> Seq("date"))

  • partition(p: A => Boolean): (Seq[A], Seq[A]):
    述語pを満たす要素と満たさない要素の2つのSeqに分割し、タプルとして返します。
    scala
    val numbers = Seq(1, 2, 3, 4, 5, 6)
    val (evens, odds) = numbers.partition(_ % 2 == 0) // (Seq(2, 4, 6), Seq(1, 3, 5))

検索・判定系

Seq内の要素を検索したり、特定の条件を満たすか判定したりするメソッドです。

  • find(p: A => Boolean): Option[A]:
    述語pを満たす最初の要素を見つけてSome(element)として返します。見つからなければNoneを返します。
    scala
    val numbers = Seq(10, 20, 30, 40)
    val found = numbers.find(_ > 25) // Some(30)
    val notFound = numbers.find(_ > 50) // None

  • exists(p: A => Boolean): Boolean:
    述語pを満たす要素が少なくとも1つ存在するかどうかを判定します。
    scala
    val numbers = Seq(1, 2, 3, 4)
    println(numbers.exists(_ % 2 == 0)) // true
    println(numbers.exists(_ > 10)) // false

  • forall(p: A => Boolean): Boolean:
    すべての要素が述語pを満たすかどうかを判定します。
    scala
    val numbers = Seq(2, 4, 6)
    println(numbers.forall(_ % 2 == 0)) // true
    println(numbers.forall(_ < 5)) // false

  • contains[A1 >: A](elem: A1): Boolean:
    指定された要素が含まれているかどうかを判定します。
    scala
    val seq = Seq(1, 2, 3)
    println(seq.contains(2)) // true
    println(seq.contains(4)) // false

  • containsSlice[B](that: Seq[B]): Boolean:
    このSeqが別のSeqを部分列として含んでいるかどうかを判定します。
    scala
    val seq = Seq(1, 2, 3, 4, 5)
    println(seq.containsSlice(Seq(2, 3))) // true
    println(seq.containsSlice(Seq(3, 1))) // false

  • startsWith[B](that: Seq[B]): Boolean:
    このSeqが別のSeqで始まっているかどうかを判定します。
    scala
    val seq = Seq(1, 2, 3, 4, 5)
    println(seq.startsWith(Seq(1, 2))) // true
    println(seq.startsWith(Seq(0, 1))) // false

  • endsWith[B](that: Seq[B]): Boolean:
    このSeqが別のSeqで終わっているかどうかを判定します。
    scala
    val seq = Seq(1, 2, 3, 4, 5)
    println(seq.endsWith(Seq(4, 5))) // true
    println(seq.endsWith(Seq(5, 6))) // false

  • indexOf[B >: A](elem: B): Int / lastIndexOf[B >: A](elem: B): Int:
    指定された要素が最初または最後に現れるインデックスを返します。見つからなければ-1を返します。
    scala
    val seq = Seq(1, 2, 1, 3)
    println(seq.indexOf(1)) // 0
    println(seq.lastIndexOf(1)) // 2
    println(seq.indexOf(4)) // -1

  • indexOfSlice[B >: A](that: Seq[B]): Int / lastIndexOfSlice[B >: A](that: Seq[B]): Int:
    別のSeqが最初または最後に部分列として現れる開始インデックスを返します。見つからなければ-1を返します。
    scala
    val seq = Seq(1, 2, 3, 2, 3, 4)
    println(seq.indexOfSlice(Seq(2, 3))) // 1
    println(seq.lastIndexOfSlice(Seq(2, 3))) // 3
    println(seq.indexOfSlice(Seq(3, 4))) // 4

部分列操作

  • slice(from: Int, until: Int): Seq[A]:
    指定されたインデックス範囲(fromからuntilの直前まで)の部分列を新しいSeqとして返します。
    scala
    val seq = Seq(10, 20, 30, 40, 50)
    val subSeq = seq.slice(1, 4) // Seq(20, 30, 40) (インデックス 1, 2, 3 の要素)

  • patch[B >: A](from: Int, patch: Seq[B], replaced: Int): Seq[B]:
    指定されたインデックスfromからreplaced個の要素を、別のSeqであるpatchの要素で置き換えた新しいSeqを返します。要素の挿入(replaced = 0)や削除(patch = Seq.empty)にも利用できます。
    “`scala
    val seq = Seq(1, 2, 3, 4, 5)
    // インデックス 2 から 2個 (3, 4) を Seq(10, 11, 12) で置き換える
    val patchedSeq = seq.patch(2, Seq(10, 11, 12), 2) // Seq(1, 2, 10, 11, 12, 5)

    // インデックス 2 に Seq(10, 11) を挿入 (0個置き換え)
    val insertedSeq = seq.patch(2, Seq(10, 11), 0) // Seq(1, 2, 10, 11, 3, 4, 5)

    // インデックス 2 から 2個 (3, 4) を削除 (空のSeqで置き換え)
    val deletedSeq = seq.patch(2, Seq.empty, 2) // Seq(1, 2, 5)
    “`

他のコレクションへの変換

Seqを他のコレクション型に変換するメソッドです。

  • toSeq, toList, toVector, toArray, toSet, toMap:
    これらのメソッドは、現在のSeqの要素を含む、指定された型の新しいコレクションを作成します。
    “`scala
    val seq = Seq(1, 2, 3)
    val list = seq.toList // List(1, 2, 3)
    val vector = seq.toVector // Vector(1, 2, 3)
    val array = seq.toArray // ArrayInt
    val set = seq.toSet // Set(1, 2, 3)

    val pairs = Seq((“a”, 1), (“b”, 2))
    val map = pairs.toMap // Map(“a” -> 1, “b” -> 2)
    ``
    要素の型によっては
    toSettoMap`で重複が失われたり、型エラーになったりする場合がある点に注意が必要です。

その他の便利メソッド

  • mkString(separator: String): String / mkString(start: String, separator: String, end: String): String:
    Seqの要素を文字列として結合します。セパレーターや、開始/終了文字列を指定できます。
    scala
    val seq = Seq("a", "b", "c")
    println(seq.mkString(",")) // "a,b,c"
    println(seq.mkString("[", ", ", "]")) // "[a, b, c]"

  • isEmpty: Boolean / nonEmpty: Boolean:
    Seqが空かどうかを判定します。
    scala
    val seq1 = Seq(1, 2)
    val seq2 = Seq.empty[Int]
    println(seq1.isEmpty) // false
    println(seq2.isEmpty) // true
    println(seq1.nonEmpty) // true
    println(seq2.nonEmpty) // false

  • size: Int / length: Int:
    Seqに含まれる要素の数を返します。Seqに対してはlengthが一般的ですが、Iterableレベルではsizeも定義されており、同じ意味で使われます。
    scala
    val seq = Seq(1, 2, 3)
    println(seq.length) // 3

  • indices: Range:
    Seqの有効なインデックスの範囲を返します。
    scala
    val seq = Seq("a", "b", "c")
    println(seq.indices) // Range(0, 1, 2)
    println(seq.indices.toList) // List(0, 1, 2)

これはSeqおよびその親トレイトであるIterableで定義されているメソッドのほんの一部です。Scalaのコレクションライブラリには非常に豊富なメソッドが用意されており、組み合わせることで複雑なデータ処理も簡潔に記述できます。公式ドキュメントを参照することで、さらに多くのメソッドを見つけることができます。

List vs Vector vs ArraySeq vs ArrayBuffer

ScalaのSeqには様々な実装がありますが、特にイミュータブルなListVector、そしてミュータブルなArrayBufferはよく使われます。ArraySeqも特定のケースで有効です。これらの違いと使い分けは、アプリケーションのパフォーマンスに大きく影響するため、理解が重要です。

内部実装と特性

実装クラス ミュータビリティ 内部構造 ランダムアクセス (読込) 先頭への追加/削除 末尾への追加/削除 中間への挿入/削除 (イミュータブル操作) 中間での更新 (イミュータブル操作)
List Immutable 単方向連結リスト O(n) O(1) O(n) O(n) O(n)
Vector Immutable 平衡木構造 O(log n) / 償却 O(1) O(log n) O(log n) / 償却 O(1) O(log n) O(log n)
ArraySeq Immutable Wrapper Java配列 O(1) O(n) O(n) O(n) O(1) (新しいSeqを生成)
ArrayBuffer Mutable 可変長配列 O(1) O(n) O(1) (償却) O(n) O(1)
  • List:

    • 利点: 先頭への追加・削除、および先頭からのシーケンシャルアクセス(head, tail, map, filterなど)が非常に高速 (O(1)) です。再帰的な処理と相性が良いです。
    • 欠点: ランダムアクセスや末尾への追加・削除、中間への挿入・削除は遅いです (O(n))。
    • 使い分け:
      • 要素をリストの先頭に頻繁に追加したり削除したりする場合。
      • ストリーム処理のように、要素を順次処理していく場合。
      • 再帰的なアルゴリズムをリスト構造に対して適用する場合。
      • リストが比較的小さい場合。
  • Vector:

    • 利点: イミュータブルでありながら、高速なランダムアクセス (O(log n))、高速な末尾への追加 (償却O(1))、および効率的なイミュータブルな更新・挿入・削除 (O(log n)) を実現しています。要素数が増えてもパフォーマンスの劣化が緩やかです。
    • 欠点: Listの先頭操作 (O(1)) よりはわずかに遅い場合があります (O(log n))。
    • 使い分け:
      • イミュータブルなSeqが必要で、かつランダムアクセスや末尾への追加、中間での更新・挿入・削除が頻繁に行われる場合。
      • 要素数が多くなる可能性がある場合。
      • デフォルトのイミュータブルなSeqとして迷ったらVectorを選ぶのが無難なケースが多いです。
  • ArraySeq:

    • 利点: Java配列をラップしているため、ランダムアクセス (O(1)) が非常に高速です。イミュータブルな更新 (新しいArraySeqを返す) も要素数に依存せず高速 (O(1)) です。
    • 欠点: 要素の追加・削除、中間への挿入は非常にコストが高いです (O(n))。
    • 使い分け:
      • イミュータブルなSeqが必要で、かつサイズが固定またはほとんど変わらず、ランダムアクセスと(置換による)更新が頻繁に行われる場合。
      • 既存のJava配列をScalaコレクションとして扱いたい場合。
  • ArrayBuffer:

    • 利点: ミュータブルであり、末尾への要素の追加 (償却O(1))、ランダムアクセス (O(1))、およびミュータブルな更新 (O(1)) が非常に高速です。
    • 欠点: 中間への要素の挿入・削除は、後続要素の移動が必要なためコストが高いです (O(n))。ミュータブルであるため、並行処理での扱いには注意が必要です。
    • 使い分け:
      • 要素を頻繁に追加・削除・更新する必要があり、ミュータビリティが許容される場合。
      • 特に末尾への追加が主に行われる場合。
      • パフォーマンスが重視される場合(ただし、ミュータビリティのリスクを理解して使用する)。

選択のポイント

  • イミュータブルが必要か、ミュータブルで良いか?
    • イミュータブルは並行処理で安全で、関数型プログラミングの思想にも合致します。特別な理由がなければイミュータブルを選びましょう。
    • ミュータブルは単一スレッドでの高速な変更操作が必要な場合に検討します。
  • どんな操作が頻繁に行われるか?
    • 先頭への追加/削除が多い -> List (Immutable) または ListBuffer (Mutable)
    • ランダムアクセスが多い -> Vector (Immutable), ArraySeq (Immutable), ArrayBuffer (Mutable)
    • 末尾への追加/削除が多い -> Vector (Immutable), ArrayBuffer (Mutable)
    • 中間への挿入/削除が多い -> ほとんどのSeqではコストが高い (O(n))。頻繁なら他のデータ構造(例: collection.immutable.TreeSeqMapなど特定の目的を持つコレクション)を検討するか、操作を工夫する必要があります。イミュータブルなSeqで中間要素を扱うならVectorが最もバランスが良いです。
  • 要素数はどれくらいになるか?
    • 要素数が少ない場合は、ほとんどのSeqの実装で大きなパフォーマンス差は出ません。
    • 要素数が多くなるにつれて、各実装の漸近的な計算量の違いが顕著になります。大規模データではVectorArrayBufferが有利になることが多いです。
  • 既存のJavaコードとの連携は必要か?
    • Java配列との相互運用が必要ならArraySeqArrayBufferが便利です。

Scala 2.13 以降の Seq:
Scala 2.13からは、デフォルトのイミュータブルなSeqのデフォルト実装がVectorになりました(以前はList)。これは、Vectorが多くの操作でバランスの取れたパフォーマンスを提供するためです。そのため、特に指定せずにSeq(...)と書いた場合はVectorが生成されます(ただし、コンパイル時や実行環境によってはListになる場合もあります。特定の性能が必要ならList(...)Vector(...)と明示的に書くべきです)。

LazyList (旧 Stream) について

LazyList[A]は、要素が遅延評価されるイミュータブルなSeqです。これはStreamという名前で知られていましたが、Scala 2.13でLazyListに改名されました。

  • 遅延評価 (Lazy Evaluation):
    LazyListの最大の特徴は、要素が必要とされるまで計算されないことです。例えば、lazyList.map(...)のような変換操作を行っても、その新しいLazyListの要素が実際にアクセスされるまで、mapの関数は適用されません。
  • 無限シーケンス:
    遅延評価のおかげで、理論上無限の要素を持つシーケンスを表現できます。
    scala
    // 無限の整数 LazyList
    val infiniteNats: LazyList[Int] = LazyList.from(1)
    println(infiniteNats.take(5).toList) // List(1, 2, 3, 4, 5) // 最初の5要素だけが計算される
  • メモ化 (Memoization):
    一度計算された要素はメモ化され、再利用されます。これにより、同じ要素に複数回アクセスしても計算が繰り返されることはありません。ただし、これはメモリを消費する可能性があるという副作用も持ちます。シーケンスの先頭への参照が残っていると、計算済みの要素がメモリから解放されずに保持され続け、メモリリークのように見えることがあります。

使い分け:

  • 非常に大きな、あるいは無限のシーケンスを扱いたい場合。
  • 計算コストの高い要素があり、その全てが必要になるわけではない場合。
  • パイプライン処理で、要素が順次生成され、すぐに消費されるような場合。

注意点:

  • メモ化によるメモリ消費に注意が必要です。特に、LazyListの先頭部分への参照を保持したまま末尾に近い要素を処理し続けると、途中の要素が解放されずメモリを圧迫することがあります。
  • 遅延評価の挙動を理解していないと、意図しないタイミングで計算が行われたり、計算が行われなかったりすることがあります。

イミュータブル vs ミュータブルな Seq

イミュータブルなSeq (List, Vector, ArraySeq, LazyListなど) とミュータブルなSeq (ArrayBuffer, ListBufferなど) の選択は、プログラムの設計において重要な決定事項です。

イミュータブルなSeq

  • 利点:

    • 安全性: 一度作成されたら変更されないため、複数のスレッドや異なる部分から同時にアクセスされても安全です。ロックなどの同期処理が不要になります。並行プログラミングや分散システムと相性が良いです。
    • 予測可能性: コレクションの状態が変化しないため、コードの挙動が理解しやすく、デバッグが容易になります。関数呼び出しによる副作用がありません。
    • 参照透過性: 同じ入力に対して常に同じ出力を返す関数を書きやすくなります。
    • 構造共有: 多くのイミュータブルコレクション実装(特にListVector)は、変更操作時に可能な限り既存の構造を共有することで、メモリ使用量とパフォーマンスを最適化します。例えば、list.tailは新しいリストを生成しますが、元のリストと末尾部分の構造を共有します。
  • 欠点:

    • 変更コスト: 見かけ上の「変更」(要素の追加・削除など)は、実際には新しいコレクションを生成することなので、そのコストがかかります。特に、頻繁に大量の変更が中間部分で行われる場合は、ミュータブルなコレクションより効率が悪くなることがあります。

ミュータブルなSeq

  • 利点:

    • パフォーマンス: in-placeでの変更操作は、多くの場合、新しいコレクションを生成するよりも高速でメモリ効率が良いです。特に、単一スレッド内で大規模なコレクションを頻繁に変更する場合に有利です。
    • Javaコレクションとの親和性: JavaのListArrayListのようなミュータブルなコレクションと似た操作感です。
  • 欠点:

    • 安全性リスク: 複数のスレッドから同時にアクセス・変更されると、競合状態(race condition)が発生し、予期しない結果やクラッシュを引き起こす可能性があります。明示的な同期処理(ロックなど)が必要です。
    • コードの複雑性: コレクションの状態がいつ、どこで変更されるかを追跡するのが難しくなり、コードが複雑化し、デバッグが困難になることがあります。副作用が発生しやすくなります。

どちらを選ぶべきか?

  • 基本的にはイミュータブルなSeqを選択する: Scalaでは、特別な理由がない限りイミュータブルなコレクションを使用することが推奨されます。これにより、安全で予測可能なコードを書きやすくなります。特に、共有される可能性のあるデータや、複数のスレッドからアクセスされるデータにはイミュータブルなSeqを使用すべきです。
  • パフォーマンスがクリティカルで、かつミュータビリティが安全に管理できる場合にミュータブルを検討する: 単一スレッド内でのローカルな変数としてコレクションを使用し、パフォーマンスが非常に重要であるような場合には、ArrayBufferなどのミュータブルなSeqの使用を検討できます。例えば、大量の要素を一時的に構築し、最後にイミュータブルなコレクションに変換する場合などです(ListBufferがこの用途に適しています)。

ミュータブルなコレクションを使用する場合でも、それを関数から返したり、他のオブジェクトに渡したりする際には、必要に応じてイミュータブルなコレクション(toVectortoListなど)に変換することで、その後の変更を防ぎ、安全性を高めることができます。

実践的な使い方のヒント

map, filter, fold などの関数型スタイルでの操作

Scalaのコレクション操作の強力さは、map, filter, reduce, foldなどの高階関数を活用できる点にあります。これらのメソッドは、ループを使わずにコレクションに対する複雑な変換や集計処理を簡潔かつ表現豊かに記述できます。

例えば、「数値のSeqから偶数だけを選び、それぞれの値を2倍にして、その合計を計算する」という処理を考えてみましょう。

手続き型スタイル (ループ):

scala
val numbers = Seq(1, 2, 3, 4, 5, 6)
var sumOfDoubledEvens = 0
for (num <- numbers) {
if (num % 2 == 0) {
sumOfDoubledEvens += num * 2
}
}
println(sumOfDoubledEvens) // 24 (2*2 + 4*2 + 6*2 = 4 + 8 + 12 = 24)

関数型スタイル (高階関数):

scala
val numbers = Seq(1, 2, 3, 4, 5, 6)
val sumOfDoubledEvens = numbers
.filter(_ % 2 == 0) // 偶数だけを選択 (Seq(2, 4, 6))
.map(_ * 2) // それぞれを2倍にする (Seq(4, 8, 12))
.sum // 合計を計算 (24)
println(sumOfDoubledEvens) // 24

関数型スタイルの方が、各操作の意図(何をフィルタリングし、何をマッピングし、何を合計するか)が明確に読み取れ、コードが簡潔になります。また、これらのメソッドは内部的に効率的な実装(並列コレクションでの実行など)を持つこともあります。

パフォーマンスを考慮した実装選択

前述のように、操作のパターンに応じて適切なSeq実装を選択することが重要です。

  • 構築後にあまり変更せず、ランダムアクセスが多いなら Vector または ArraySeq
  • 先頭への追加・削除が多いなら List
  • 末尾への追加が多く、ミュータブルで良いなら ArrayBuffer

もしパフォーマンスが問題になった場合は、まず使用しているSeqの実装と、頻繁に行われている操作の計算量を確認してみてください。

エラーハンドリング (OptionTryとの組み合わせ)

head, last, min, max, reduceなどのメソッドは、空のSeqに対して呼び出すと例外をスローする可能性があります。これを避けるためには、事前にisEmptyでチェックするか、headOption, lastOption, minOption, maxOption, reduceOptionのようにOptionを返すメソッドを利用するのがより安全な Scalaスタイルです。

“`scala
val emptySeq: Seq[Int] = Seq.empty

// 安全な方法
val firstElement = emptySeq.headOption // None
val minimum = emptySeq.minOption // None

// 例外が発生する可能性のある方法
// val firstElementUnsafe = emptySeq.head // java.lang.UnsupportedOperationException
“`

パターンマッチングとSeq

ListなどのSeqは、パターンマッチングと非常に相性が良いです。特に連結リストであるListは、:: (consオペレーター) を使って再帰的な構造をパターンマッチングで分解できます。

“`scala
def describeSeqA: String = seq match {
case Nil => “Empty List/Seq” // List.empty と同じ
case head :: tail => s”Head: $head, Tail: $tail” // List のパターンマッチ
case element +: Seq() => s”Single element: $element” // Seq の単一要素パターンマッチ (Vectorなども含む)
case head +: tail => s”Head: $head, Tail is a Seq with ${tail.length} elements” // Seq のパターンマッチ (List以外も含む)
case _ => s”A Seq of length ${seq.length}” // その他のSeq (ArraySeqなど)
}

println(describeSeq(List(1, 2, 3))) // Head: 1, Tail: List(2, 3)
println(describeSeq(Vector(“a”, “b”))) // Head: a, Tail is a Seq with 1 elements
println(describeSeq(Seq(10))) // Single element: 10
println(describeSeq(Seq.empty)) // Empty List/Seq
println(describeSeq(ArraySeq(true))) // Single element: true
``::List特有のパターンマッチングですが、+: Seq()+: tailのようなパターンはSeq全般(List,Vector,ArraySeq`など)に適用できます。

よくある落とし穴と注意点

  1. イミュータブルなSeqの操作は新しいSeqを返すことを理解する: list.filter(...)vector.map(...)のような操作は、元のlistvectorを変更しません。結果を利用するには、返された新しいコレクションを変数に代入する必要があります。
    scala
    val original = Seq(1, 2, 3)
    original.filter(_ > 1) // この行だけでは original は変わらない
    val filtered = original.filter(_ > 1) // filtered は Seq(2, 3), original は Seq(1, 2, 3) のまま

  2. インデックスアクセス時の境界チェック (IndexOutOfBoundsException): seq(index)のようにインデックスを使ってアクセスする場合、インデックスが有効な範囲外だと例外が発生します。ユーザーからの入力など、信頼できないインデックスを使用する場合は、事前にインデックスが有効かチェックするか、isDefinedAtなどのメソッドを検討してください。ただし、通常はインデックスアクセスよりもmap, filterなどの高階関数を使う方が安全でスケーラブルなコードになります。

  3. head/tailなどの呼び出し時の空Seqチェック (UnsupportedOperationException): 空のSeqに対してheadtailなどを呼び出すと例外が発生します。安全に扱いたい場合はheadOptionなどを使用してください。

  4. ミュータブルなSeqの共有による意図しない変更: ArrayBufferなどのミュータブルなコレクションを複数の箇所で共有している場合、ある箇所での変更が他の箇所に影響を与え、バグの原因となることがあります。共有される可能性のあるデータはイミュータブルにするのが基本です。

まとめ

ScalaのSeqトレイトは、順序付けられた要素の集まりを表現するための基盤です。要素へのインデックスアクセスを可能にするapplyメソッドを持ち、Iterableから継承した豊富なコレクション操作メソッド(map, filter, foldなど)を提供します。

Seqには、イミュータブルなList, Vector, ArraySeq, LazyListや、ミュータブルなArrayBuffer, ListBufferなど、様々な実装が存在します。それぞれの実装は内部構造やパフォーマンス特性が異なり、得意とする操作が異なります。

  • List: 先頭操作に強く、再帰と相性が良い。ランダムアクセスは遅い。
  • Vector: ランダムアクセス、末尾追加、イミュータブルな更新に強く、バランスが良い。要素数が多い場合や、ランダムアクセス・更新が頻繁な場合に推奨されるイミュータブルなSeq。
  • ArraySeq: Java配列ベースで、ランダムアクセスとイミュータブルな更新が非常に速い。追加・削除は遅い。
  • LazyList: 遅延評価されるため、無限シーケンスやコストの高い要素のシーケンスに。
  • ArrayBuffer: ミュータブルで、末尾追加、ランダムアクセス、ミュータブルな更新が速い。単一スレッド内での高速な変更に適する。

Scalaでは、安全性や予測可能性の観点から、特に理由がなければイミュータブルなSeq(デフォルトはVector)を使用することが推奨されます。パフォーマンスがボトルネックになり、かつミュータビリティのリスクを管理できる場合にのみ、ミュータブルなSeqを検討するべきです。

map, filter, foldなどの高階関数を積極的に利用することで、Scalaのコレクション操作をより簡潔かつ表現豊かに記述できます。また、Optionやパターンマッチングを活用することで、安全で堅牢なコードを書くことができます。

この記事を通して、ScalaのSeqとその主要な実装、そして効果的な使い方について理解を深められたなら幸いです。Scalaの強力なコレクションライブラリを活用して、より高品質なアプリケーション開発を目指しましょう!

参考文献 / さらに学ぶために


約5000語となるように、各セクションを詳細に記述しました。特にメソッドの説明やList vs Vector vs ArraySeq vs ArrayBufferの比較部分に多くの語数を使っています。この内容が、ScalaのSeqについて深く理解するための一助となれば幸いです。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール