はい、承知いたしました。ScalaのSeq
について、定義から使い方、List
との比較までを網羅的に解説する記事を作成します。約5000語を目指し、詳細な説明を含めます。
【Scala】Seqの全て!定義から使い方、Listとの比較まで徹底解説
はじめに
Scalaは、関数型プログラミングとオブジェクト指向プログラミングを統合した強力な言語です。その魅力の一つに、洗練されたコレクションライブラリがあります。Scalaのコレクションライブラリは、イミュータブル(不変)性とミュータブル(可変)性の両方をサポートしつつ、豊富な高階関数を提供することで、データの操作を安全かつ簡潔に行うことを可能にしています。
コレクションライブラリの中でも、最も頻繁に利用されるデータ構造の一つが「シーケンス(Sequence)」、ScalaではSeq
トレイトによって表現されるものです。Seq
は「順序付けられた要素の集まり」を抽象化したものであり、プログラミングにおけるリスト、配列、ベクトルなど、多くの基本的なデータ構造の基盤となっています。
しかし、一口にSeq
と言っても、その実態である具体的なクラス(List
, Vector
, ArraySeq
, ArrayBuffer
など)は内部構造やパフォーマンス特性が大きく異なります。これらの違いを理解せずに利用すると、意図しないパフォーマンス劣化を招く可能性もあります。
この記事では、ScalaのSeq
トレイトについて、その定義から始め、主要なメソッド、具体的な実装クラス(特にList
とVector
)、そしてイミュータブル/ミュータブルなSeq
の使い分けまでを徹底的に解説します。この記事を読むことで、あなたはScalaのSeq
を自在に操り、より効率的かつ安全なコードを書けるようになるでしょう。
さあ、ScalaのSeq
の世界へ飛び込みましょう!
Scalaコレクションライブラリの階層におけるSeqの位置づけ
Scalaのコレクションライブラリは、トレイトの階層構造によって設計されています。この階層を理解することは、各コレクション型の特性や利用可能なメソッドを把握する上で非常に重要です。
最上位には、ほとんどのコレクションが継承するIterable
トレイトがあります。Iterable
は、要素を順番に辿ることができる(イテレーションできる)コレクションを表します。foreach
, map
, filter
, foldLeft
など、多くの高階関数はIterable
トレイトで定義されており、Iterable
を継承するすべてのコレクションで利用できます。
Iterable
の直下には、主要な3つのトレイトがあります。
Seq
(Sequence): 順序付けられた要素の集まり。要素にはインデックスでアクセスできます。Set
: 重複しない要素の集まり。要素の順序は保証されません(ただし、SortedSet
などの実装を除く)。Map
: キーと値のペアの集まり。キーは重複しません。
この記事の主役であるSeq
は、この中で「順序付けられている」という特徴を持つコレクションを抽象化しています。つまり、Seq
はIterable
の性質(イテレーション可能)に加え、要素が特定の順番で並んでおり、その順番(インデックス)を指定して要素を取り出したり、並び替えたりといった操作が可能であることを保証します。
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点です。
- 順序付けられている (Ordered): 要素が特定の順番で並んでいます。要素の追加順序などがその順番を決定します。
- インデックスアクセスが可能 (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
(特にList
やVector
)を想定して説明します。
生成
Seq
を生成する最も一般的な方法はいくつかあります。
-
コンパニオンオブジェクトの
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]
は、指定された型の空のシーケンスを生成する効率的な方法です。 -
特定の型(
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) -
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
を実装しており、効率的なインデックスアクセスが可能です。 -
fill
とtabulate
:
同じ要素を繰り返したり、インデックスに基づいて要素を生成したりするのに便利です。
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)
は、要素elem
をn
回繰り返すSeqを生成します。
tabulate(n)(f)
は、インデックスi
(0からn-1
まで) を関数f
に適用して要素を生成するSeqを生成します。
要素へのアクセス
Seq
は順序付けられているため、インデックスや位置に基づいて要素にアクセスできます。
-
インデックスアクセス (
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 が発生 -
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!
“` -
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
に直接要素を「削除」する汎用的なメソッドは、インデックス指定では提供されていません。インデックスで指定して要素を削除したい場合は、take
とdrop
を組み合わせて新しい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の主要メソッド
Seq
はIterable
トレイトを継承しているため、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
として返します。filter
とmap
を組み合わせたようなものです。
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]
:
述語p
がtrue
を返す要素のみを新しい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]
:
述語p
がfalse
を返す要素のみを新しい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]
:
述語p
がtrue
を返す間、先頭から要素を取り出し、最初の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]
:
述語p
がtrue
を返す間、先頭から要素を削除し、最初の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])
:
指定されたインデックスn
でSeq
を分割し、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)
``
toSet
要素の型によってはや
toMap`で重複が失われたり、型エラーになったりする場合がある点に注意が必要です。
その他の便利メソッド
-
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
には様々な実装がありますが、特にイミュータブルなList
とVector
、そしてミュータブルな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
の実装で大きなパフォーマンス差は出ません。 - 要素数が多くなるにつれて、各実装の漸近的な計算量の違いが顕著になります。大規模データでは
Vector
やArrayBuffer
が有利になることが多いです。
- 要素数が少ない場合は、ほとんどの
- 既存のJavaコードとの連携は必要か?
- Java配列との相互運用が必要なら
ArraySeq
やArrayBuffer
が便利です。
- Java配列との相互運用が必要なら
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
-
利点:
- 安全性: 一度作成されたら変更されないため、複数のスレッドや異なる部分から同時にアクセスされても安全です。ロックなどの同期処理が不要になります。並行プログラミングや分散システムと相性が良いです。
- 予測可能性: コレクションの状態が変化しないため、コードの挙動が理解しやすく、デバッグが容易になります。関数呼び出しによる副作用がありません。
- 参照透過性: 同じ入力に対して常に同じ出力を返す関数を書きやすくなります。
- 構造共有: 多くのイミュータブルコレクション実装(特に
List
やVector
)は、変更操作時に可能な限り既存の構造を共有することで、メモリ使用量とパフォーマンスを最適化します。例えば、list.tail
は新しいリストを生成しますが、元のリストと末尾部分の構造を共有します。
-
欠点:
- 変更コスト: 見かけ上の「変更」(要素の追加・削除など)は、実際には新しいコレクションを生成することなので、そのコストがかかります。特に、頻繁に大量の変更が中間部分で行われる場合は、ミュータブルなコレクションより効率が悪くなることがあります。
ミュータブルなSeq
-
利点:
- パフォーマンス: in-placeでの変更操作は、多くの場合、新しいコレクションを生成するよりも高速でメモリ効率が良いです。特に、単一スレッド内で大規模なコレクションを頻繁に変更する場合に有利です。
- Javaコレクションとの親和性: Javaの
List
やArrayList
のようなミュータブルなコレクションと似た操作感です。
-
欠点:
- 安全性リスク: 複数のスレッドから同時にアクセス・変更されると、競合状態(race condition)が発生し、予期しない結果やクラッシュを引き起こす可能性があります。明示的な同期処理(ロックなど)が必要です。
- コードの複雑性: コレクションの状態がいつ、どこで変更されるかを追跡するのが難しくなり、コードが複雑化し、デバッグが困難になることがあります。副作用が発生しやすくなります。
どちらを選ぶべきか?
- 基本的にはイミュータブルなSeqを選択する: Scalaでは、特別な理由がない限りイミュータブルなコレクションを使用することが推奨されます。これにより、安全で予測可能なコードを書きやすくなります。特に、共有される可能性のあるデータや、複数のスレッドからアクセスされるデータにはイミュータブルな
Seq
を使用すべきです。 - パフォーマンスがクリティカルで、かつミュータビリティが安全に管理できる場合にミュータブルを検討する: 単一スレッド内でのローカルな変数としてコレクションを使用し、パフォーマンスが非常に重要であるような場合には、
ArrayBuffer
などのミュータブルなSeq
の使用を検討できます。例えば、大量の要素を一時的に構築し、最後にイミュータブルなコレクションに変換する場合などです(ListBuffer
がこの用途に適しています)。
ミュータブルなコレクションを使用する場合でも、それを関数から返したり、他のオブジェクトに渡したりする際には、必要に応じてイミュータブルなコレクション(toVector
やtoList
など)に変換することで、その後の変更を防ぎ、安全性を高めることができます。
実践的な使い方のヒント
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
の実装と、頻繁に行われている操作の計算量を確認してみてください。
エラーハンドリング (Option
やTry
との組み合わせ)
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`など)に適用できます。
よくある落とし穴と注意点
-
イミュータブルなSeqの操作は新しいSeqを返すことを理解する:
list.filter(...)
やvector.map(...)
のような操作は、元のlist
やvector
を変更しません。結果を利用するには、返された新しいコレクションを変数に代入する必要があります。
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) のまま -
インデックスアクセス時の境界チェック (
IndexOutOfBoundsException
):seq(index)
のようにインデックスを使ってアクセスする場合、インデックスが有効な範囲外だと例外が発生します。ユーザーからの入力など、信頼できないインデックスを使用する場合は、事前にインデックスが有効かチェックするか、isDefinedAt
などのメソッドを検討してください。ただし、通常はインデックスアクセスよりもmap
,filter
などの高階関数を使う方が安全でスケーラブルなコードになります。 -
head
/tail
などの呼び出し時の空Seqチェック (UnsupportedOperationException
): 空のSeq
に対してhead
やtail
などを呼び出すと例外が発生します。安全に扱いたい場合はheadOption
などを使用してください。 -
ミュータブルな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の強力なコレクションライブラリを活用して、より高品質なアプリケーション開発を目指しましょう!
参考文献 / さらに学ぶために
- Scala公式ドキュメント: Collections Overview
https://docs.scala-lang.org/overviews/collections/introduction.html - Scala公式ドキュメント: Sequences
https://docs.scala-lang.org/overviews/collections/sequences.html - “Programming in Scala” (Odersky, Spoon, Venners 著) – Scalaのコレクションライブラリについて詳細な章があります。
約5000語となるように、各セクションを詳細に記述しました。特にメソッドの説明やList vs Vector vs ArraySeq vs ArrayBufferの比較部分に多くの語数を使っています。この内容が、ScalaのSeq
について深く理解するための一助となれば幸いです。