はい、承知いたしました。ScalaのSeqの使い方について、初心者向けの詳細な解説記事(約5000語)を記述します。記事内容を直接表示します。
【Scala】Seqの使い方を徹底解説!初心者向け入門ガイド
はじめに:ScalaコレクションとSeqの重要性
Scalaプログラミングにおいて、データ構造を扱うことは避けて通れません。複数のデータをひとまとめにして管理し、様々な操作を行う必要が頻繁に発生します。このようなデータの集まりを扱うための強力なツールが、Scalaの「コレクション」ライブラリです。
Scalaのコレクションライブラリは非常に豊富で、リスト、セット、マップなど、様々な種類のコレクションを提供しています。これらのコレクションは、堅牢で効率的、そして関数型プログラミングのパラダイムと非常に相性の良い設計になっています。
その中でも特に基本的な、そして非常によく使われるコレクションが Seq (シーク) です。Seqは「Sequence」(順序)の略で、名前の通り、要素が特定の順序で並んだコレクションを表します。
なぜSeqが重要なのでしょうか?
- 順序付き: 要素が追加された(あるいは定義された)順序を保持します。これにより、インデックスを使って特定の位置の要素にアクセスしたり、要素の並び順に意味を持たせたりすることができます。
- 多様な操作:
Seqトレイトは、要素の追加、削除、変換、検索、集約など、数多くの便利な操作(メソッド)を提供しています。これらの操作の多くは、元のSeqを変更せずに新しいSeqを生成する、いわゆる「非破壊的」な操作です(immutable)。 - ポリモーフィズム:
Seqはトレイト(trait)であり、抽象的な型です。具体的な実装にはListやVectorなどがありますが、多くの場合は操作対象をSeq型として扱うことで、コードの汎用性が高まります。これは、どの種類の順序付きコレクションを渡されても同じように処理できることを意味します。 - Immutable優先: Scalaのコレクションライブラリは、デフォルトでImmutable(不変)なコレクションを提供することを推奨しています。Immutableなコレクションは、一度生成されると内容を変更できません。これは並行処理における安全性を高めたり、プログラムの挙動を予測しやすくしたりする上で非常に大きなメリットがあります。
SeqもデフォルトではImmutableなSeqを指します。
この記事では、Scala初心者の方を対象に、Seqの基本から応用までを徹底的に解説します。Seqの概念、生成方法、基本的な操作、よく使う変換・集約メソッド、そして代表的な実装クラスであるListとVectorの違いまで、具体的なコード例を交えながら詳しく見ていきましょう。この記事を読めば、Scalaで順序付きデータを自在に扱えるようになるはずです。
さあ、ScalaのSeqの世界に飛び込みましょう!
Scalaコレクション概論:階層とImmutable/Mutable
Seqについて深く理解するために、まずはScalaのコレクションライブラリ全体の構造と、Immutable/Mutableという概念について簡単に触れておきます。
コレクションの階層
Scalaのコレクションは、トレイトの階層構造を形成しています。主な階層は以下のようになっています。
Iterable
+- Seq
| +- LinearSeq (List, Stream)
| +- IndexedSeq (Vector, ArraySeq)
+- Set
+- Map
- Iterable: すべてのコレクションの基底となるトレイトです。要素を順番に走査できる(イテレートできる)ことを保証します。
map,filter,foreachなど、多くの共通操作はIterableで定義されています。 - Seq: 要素が順序付けられているコレクションです。
Iterableを継承し、さらにインデックスによるアクセスや順序に関する操作(head,tailなど)を追加しています。Seq自体は抽象的なトレイトであり、直接インスタンスを作成することはできません。 - Set: 重複しない要素の集まりです。順序は保証されません(通常は保持されません)。
- Map: キーと値のペア(エントリ)の集まりです。キーは重複しません。順序は保証されません(通常は保持されません)。
Seqは、この階層の中で「順序付きコレクション」を代表する存在と言えます。この記事で扱うListやVectorは、このSeqトレイトを具体的に実装したクラスです。
Immutable (不変) vs Mutable (可変)
Scalaのコレクションは、大きくImmutable(不変)とMutable(可変)の2種類に分けられます。
-
Immutable Collection: 一度生成されると、その内容は二度と変更できません。要素の追加や削除、更新といった操作を行っても、元のコレクションは変化せず、操作の結果として新しいコレクションが生成されます。Scalaでは、
scala.collection.immutableパッケージに属するコレクション(例:List,Vector,Set,Mapなど)がこれにあたります。scala
val list1 = List(1, 2, 3) // immutableなList
val list2 = list1 :+ 4 // list1に4を追加した新しいListを生成
println(list1) // List(1, 2, 3) - 変わらない
println(list2) // List(1, 2, 3, 4) - 新しいリスト -
Mutable Collection: 生成された後も、その内容を変更できます。要素の追加や削除、更新は、元のコレクションに対して直接行われます。Scalaでは、
scala.collection.mutableパッケージに属するコレクション(例:ArrayBuffer,ListBuffer,HashSet,HashMapなど)がこれにあたります。“`scala
import scala.collection.mutableval buffer1 = mutable.ArrayBuffer(1, 2, 3) // mutableなArrayBuffer
buffer1 += 4 // buffer1自身に4を追加
println(buffer1) // ArrayBuffer(1, 2, 3, 4) – 変わった
“`
Scalaでは、特別な理由がない限りImmutableなコレクションを使うことが強く推奨されています。
- 安全性の向上: 並行処理において、複数のスレッドが同じコレクションに同時にアクセスしても、コレクションの内容が勝手に変わることがないため、同期処理の実装が不要になり、安全なコードが書きやすくなります。
- 予測可能性: コレクションの内容が不変であるため、どこかで予期せずコレクションが変更される心配がなくなり、プログラムの挙動が追跡しやすくなります。
- 関数型プログラミングとの親和性: 関数型プログラミングでは、「副作用のない関数」を重視します。Immutableなコレクションは、副作用なくデータ構造を扱えるため、関数型スタイルと非常に良く合います。
この記事で主に扱うSeqは、デフォルトではImmutableなSeqを指します。 特定の実装クラス(List, Vectorなど)も、特別な指定がない限りImmutableなものが使われます。MutableなSeq (scala.collection.mutable.Seq) については、後ほど少しだけ触れます。
Seqとは?基本理解
改めて、Seqとは何でしょうか?
Seq[A]は、型Aの要素を持つ、順序付けられたコレクションを表すトレイトです。
重要な特性をまとめると:
- 順序保持: 要素は特定の順序で格納されており、その順序は維持されます。
- インデックスアクセス: 0から始まる整数インデックスを使って、任意の位置の要素にアクセスできます。
- Iterableのサブタイプ:
Iterableトレイトが提供するすべての操作(map,filter,foreachなど)を利用できます。 - 追加の操作:
head,tail,apply(インデックスアクセス),indexOfなど、順序付きコレクション特有の操作が追加されています。
Seqはトレイトなので、直接インスタンスを作成することはできません。実際には、Seqを実装した具体的なクラスのインスタンスを作成し、それをSeq型として扱うことになります。
Seqの主な実装クラス
Seqトレイトを実装した代表的なクラスには、Immutableなものとして以下の2つがあります。
-
scala.collection.immutable.List: 単方向リンクリストとして実装されています。要素はリストの先頭から順にリンクで繋がっています。- 得意な操作: リストの先頭への要素の追加(
::,+:)、先頭要素の取得(head)、先頭以外すべてを取得(tail)。これらの操作は非常に効率的(定数時間 O(1))です。 - 苦手な操作: リストの末尾への要素の追加(
:+)、任意の位置へのアクセス(apply(index))、任意の位置への要素の挿入・削除・更新。これらの操作はリストの長さに比例して時間がかかります(線形時間 O(n))。 - 再帰的な処理と相性が良いです。
- 得意な操作: リストの先頭への要素の追加(
-
scala.collection.immutable.Vector: ブロック化されたバランスツリーとして実装されています。内部的には小さな配列(ブロック)を使い、それらがツリー構造で繋がっています。- 得意な操作: リストの先頭・末尾への要素の追加(
+:、:+)、任意の位置へのアクセス(apply(index))、任意の位置での要素の更新。これらの操作は非常に効率的(ほぼ定数時間、正確には要素数Nに対してlog(N)やlog_32(N)程度)です。 - 苦手な操作: Listほど先頭操作が極端に速いわけではありませんが、ほとんどの操作が効率的です。
- 大規模なデータを扱う場合や、先頭・末尾だけでなく様々な位置の操作が多い場合に適しています。Scala 2.8以降、デフォルトのImmutable
Seq実装として推奨されることが多いです。
- 得意な操作: リストの先頭・末尾への要素の追加(
その他にも、ArraySeq(配列をラップしたもの、インデックスアクセスが最速だがサイズ変更は非効率)、Stream(遅延評価されるリンクリスト、Scala 2.13でLazyListに名称変更・非推奨)などがあります。
この記事の多くの例では、特定のSeq実装ではなくSeqトレイトとして操作を説明します。これは、実際のプログラミングでは特定のコレクション型に依存せず、より汎用的なSeq型として扱うことが多いからです。
なぜSeqトレイトを使うことが多いのか?
例えば、関数が引数として順序付きコレクションを受け取る場合を考えてみましょう。
“`scala
def processSequence(seq: Seq[Int]): Seq[Int] = {
// … seqを使った処理 …
seq.map(_ * 2)
}
val myList = List(1, 2, 3)
val myVector = Vector(4, 5, 6)
val result1 = processSequence(myList) // Listを渡せる
val result2 = processSequence(myVector) // Vectorも渡せる
“`
このように、引数の型をSeqとしておくことで、ListでもVectorでも、あるいは将来登場するかもしれない別のSeq実装でも受け入れることができるようになります。これは「ポリモーフィズム」の恩恵です。関数の内部で行う操作がSeqトレイトで定義されているものであれば、具体的な実装クラスを知らなくても処理が可能です。
特に理由がない限り、関数や変数では具体的な実装クラス(ListやVector)ではなく、抽象的なトレイト(Seq, Set, Map, Iterableなど)を使うのがScalaの慣習であり、推奨されるプラクティスです。
Seqの生成方法
それでは、実際にSeqを生成する方法を見ていきましょう。
1. ファクトリメソッド Seq(...)
最も簡単で一般的な方法は、Seqオブジェクトのファクトリメソッドを使う方法です。これは、引数に渡された要素を持つImmutableなSeqを生成します。デフォルトの実装としてはVectorが使われることが多いですが、正確な実装はバージョンによって異なる場合があります。
“`scala
val numbers: Seq[Int] = Seq(1, 2, 3, 4, 5)
println(numbers) // 例: Vector(1, 2, 3, 4, 5) または List(1, 2, 3, 4, 5) など
val fruits: Seq[String] = Seq(“apple”, “banana”, “cherry”)
println(fruits) // 例: Vector(apple, banana, cherry) または List(apple, banana, cherry) など
“`
型推論によって型を省略することも可能です。
scala
val mixedSeq = Seq(1, "two", 3.0) // Seq[Any] と推論される
println(mixedSeq) // 例: Vector(1, two, 3.0)
2. 特定のSeq実装クラスを指定して生成
ListやVectorなど、特定のSeq実装クラスを指定して生成することもよく行われます。これは、その実装クラスの特性(パフォーマンスなど)を活かしたい場合に有用です。
“`scala
val myList: List[Int] = List(10, 20, 30)
println(myList) // List(10, 20, 30)
val myVector: Vector[String] = Vector(“A”, “B”, “C”)
println(myVector) // Vector(A, B, C)
“`
リストリテラル構文も使えます(実際にはListのファクトリメソッドの糖衣構文です)。
scala
val listLiteral = 1 :: 2 :: 3 :: Nil // List(1, 2, 3)
println(listLiteral) // List(1, 2, 3)
:: は要素をリストの先頭に追加する演算子で、Nilは空のリストを表します。この構文は特に再帰的なリスト処理でよく使われます。
3. 空のSeqの生成
要素を持たない空のSeqは、Seq.empty[A]または特定のクラスのempty[A]を使って生成します。型パラメータ[A]で要素の型を指定する必要があります。
“`scala
val emptyNumbers: Seq[Int] = Seq.empty[Int]
println(emptyNumbers) // 例: Vector()
val emptyStrings: List[String] = List.empty[String]
println(emptyStrings) // List()
val emptyAny: Vector[Any] = Vector.empty[Any]
println(emptyAny) // Vector()
“`
4. 範囲からSeqを生成
数値の範囲からSeqを生成する場合、toSeqメソッドが便利です。
“`scala
val rangeSeq1 = (1 to 5).toSeq // 1から5までのRangeをSeqに変換
println(rangeSeq1) // 例: Vector(1, 2, 3, 4, 5)
val rangeSeq2 = (1 until 5).toSeq // 1から4までのRangeをSeqに変換
println(rangeSeq2) // 例: Vector(1, 2, 3, 4)
“`
5. 既存のコレクションから変換
他のコレクション型からSeqに変換したい場合は、toSeqメソッドを使用します。
“`scala
val set: Set[Int] = Set(1, 2, 3)
val setAsSeq: Seq[Int] = set.toSeq // 順序は保証されない
println(setAsSeq) // 例: Vector(1, 2, 3) (実際の出力順序はSetの実装に依存)
val map: Map[String, Int] = Map(“a” -> 1, “b” -> 2)
val mapAsSeq: Seq[(String, Int)] = map.toSeq // Mapのエントリ((キー, 値)のペア)のSeq
println(mapAsSeq) // 例: Vector((a,1), (b,2)) (実際の出力順序はMapの実装に依存)
“`
Seqの基本的な操作
Seqトレイトは、要素のアクセスや基本的な情報の取得など、多くの便利な基本操作を提供しています。これらの操作は、ImmutableなSeqに対しては、常に新しいSeqや値を返します。元のSeqは変更されません。
1. 要素へのアクセス
インデックス(0から始まる整数)を使って要素にアクセスできます。
apply(index: Int)またはseq(index: Int): 指定したインデックスの要素を取得します。インデックスが範囲外の場合はIndexOutOfBoundsExceptionがスローされます。
“`scala
val numbers = Seq(10, 20, 30, 40, 50)
println(numbers(0)) // 10
println(numbers.apply(2)) // 30
// println(numbers(10)) // エラー: IndexOutOfBoundsException
“`
2. 先頭・末尾要素へのアクセス
head: Seqの最初の要素を取得します。tail: Seqの最初の要素を除いた残りの要素をSeqとして取得します。last: Seqの最後の要素を取得します。init: Seqの最後の要素を除いた残りの要素をSeqとして取得します。
これらのメソッドは、Seqが空の場合はNoSuchElementExceptionをスローします。
“`scala
val numbers = Seq(10, 20, 30, 40, 50)
println(numbers.head) // 10
println(numbers.tail) // Seq(20, 30, 40, 50)
println(numbers.last) // 50
println(numbers.init) // Seq(10, 20, 30, 40)
val emptySeq: Seq[Int] = Seq.empty
// println(emptySeq.head) // エラー: NoSuchElementException
“`
安全に先頭/末尾要素を取得したい場合は、headOptionやlastOptionを使います。これらは結果をOptionでラップして返します。
“`scala
println(numbers.headOption) // Some(10)
println(emptySeq.headOption) // None
println(numbers.lastOption) // Some(50)
println(emptySeq.lastOption) // None
“`
3. 要素数の取得
lengthまたはsize: Seqに含まれる要素の数を取得します。
scala
val numbers = Seq(10, 20, 30, 40, 50)
println(numbers.length) // 5
println(numbers.size) // 5
4. 空かどうかの判定
isEmpty: Seqが空の場合にtrueを返します。nonEmpty: Seqが空でない場合にtrueを返します。
“`scala
val numbers = Seq(10, 20, 30)
val emptySeq: Seq[Int] = Seq.empty
println(numbers.isEmpty) // false
println(numbers.nonEmpty) // true
println(emptySeq.isEmpty) // true
println(emptySeq.nonEmpty) // false
“`
5. 要素の存在チェック
contains(elem: Any): 指定した要素がSeqに含まれているか判定します。exists(p: A => Boolean): 指定した条件(述語関数p)を満たす要素が少なくとも1つ存在するか判定します。forall(p: A => Boolean): Seqのすべての要素が指定した条件(述語関数p)を満たすか判定します。
“`scala
val numbers = Seq(10, 20, 30, 40, 50)
println(numbers.contains(30)) // true
println(numbers.contains(100)) // false
println(numbers.exists( > 40)) // true (50が条件を満たす)
println(numbers.exists( < 0)) // false
println(numbers.forall( > 0)) // true (すべての要素が0より大きい)
println(numbers.forall( > 10)) // false (10が条件を満たさない)
“`
6. 要素の検索
indexOf(elem: Any): 指定した要素が見つかった最初のインデックスを返します。見つからない場合は-1を返します。lastIndexOf(elem: Any): 指定した要素が見つかった最後のインデックスを返します。見つからない場合は-1を返します。find(p: A => Boolean): 指定した条件を満たす最初の要素をOptionでラップして返します。見つからない場合はNoneを返します。
“`scala
val numbers = Seq(10, 20, 30, 20, 50)
println(numbers.indexOf(20)) // 1
println(numbers.lastIndexOf(20)) // 3
println(numbers.indexOf(100)) // -1
println(numbers.find( > 30)) // Some(40)
println(numbers.find( > 100)) // None
“`
7. 連結 (++)
2つのSeqを連結して新しいSeqを作成します。
“`scala
val seq1 = Seq(1, 2, 3)
val seq2 = Seq(4, 5, 6)
val combinedSeq = seq1 ++ seq2
println(combinedSeq) // Seq(1, 2, 3, 4, 5, 6)
“`
8. 要素の追加・削除 (Immutableなので新しいSeqを生成)
ImmutableなSeqは内容を変更できないため、「要素の追加・削除」という操作は、実際には「指定した要素を追加/削除した新しいSeqを生成する」ということになります。
+:: 要素をSeqの先頭に追加します。:+: 要素をSeqの末尾に追加します。
“`scala
val numbers = Seq(2, 3, 4)
val newSeq1 = 1 +: numbers // 1を先頭に追加
println(newSeq1) // Seq(1, 2, 3, 4)
val newSeq2 = numbers :+ 5 // 5を末尾に追加
println(newSeq2) // Seq(2, 3, 4, 5)
println(numbers) // Seq(2, 3, 4) – 元のSeqは変わらない!
“`
Listの場合は、::演算子を使って先頭に追加する方が効率的で慣習的です。
scala
val myList = List(2, 3, 4)
val newList = 1 :: myList // Listの先頭に追加 (Listの場合はこれが効率的)
println(newList) // List(1, 2, 3, 4)
要素の削除は、特定の要素を指定して削除する直接的なメソッドはSeqにはありません。通常はfilterメソッドを使って、残したい要素を条件で指定する方法が使われます。
“`scala
val numbers = Seq(10, 20, 30, 40, 50)
// 30以外の要素だけを残す
val filteredSeq = numbers.filter(_ != 30)
println(filteredSeq) // Seq(10, 20, 40, 50)
// 偶数だけを残す
val evenNumbers = numbers.filter(_ % 2 == 0)
println(evenNumbers) // Seq(10, 20, 40, 50)
println(numbers) // Seq(10, 20, 30, 40, 50) – 元のSeqは変わらない!
“`
インデックスを指定して要素を削除したり、特定の位置に要素を挿入したりする場合は、patchメソッドを使う方法もありますが、filterやtake, drop, ++などの組み合わせで実現することも多いです。
“`scala
val numbers = Seq(10, 20, 30, 40, 50)
// インデックス2の要素(30)を削除
// インデックス0から2個取り、インデックス3から最後までを取り連結
val removedSeq = numbers.take(2) ++ numbers.drop(3)
println(removedSeq) // Seq(10, 20, 40, 50)
// インデックス2に要素35を挿入
// インデックス0から2個取り、要素35、インデックス2から最後までを取り連結
val insertedSeq = numbers.take(2) ++ Seq(35) ++ numbers.drop(2)
println(insertedSeq) // Seq(10, 20, 35, 30, 40, 50)
“`
Seqの変換操作 (よく使うもの)
Seqの最も強力な側面の1つは、豊富な変換操作です。これらの操作は、元のSeqを変更せずに、新しいSeqや他のコレクション、あるいは単一の値を生成します。特に、高階関数(関数を引数に取る関数)を利用した操作が多く、関数型プログラミングのスタイルでデータを処理するのに非常に適しています。
1. map: 各要素の変換
mapメソッドは、Seqの各要素に関数を適用し、その結果を新しいSeqとして返します。要素の型を変換するのによく使われます。
“`scala
val numbers = Seq(1, 2, 3, 4, 5)
// 各要素を2倍にする
val doubled = numbers.map(_ * 2)
println(doubled) // Seq(2, 4, 6, 8, 10)
val strings = Seq(“apple”, “banana”, “cherry”)
// 各要素を大文字にする
val upperCase = strings.map(_.toUpperCase)
println(upperCase) // Seq(APPLE, BANANA, CHERRY)
// 要素の型をIntからStringに変換
val numberStrings = numbers.map(_.toString)
println(numberStrings) // Seq(“1”, “2”, “3”, “4”, “5”)
“`
2. filter: 要素の絞り込み
filterメソッドは、指定した条件(述語関数)を満たす要素だけを抽出し、新しいSeqとして返します。
“`scala
val numbers = Seq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// 偶数だけを抽出
val evenNumbers = numbers.filter(_ % 2 == 0)
println(evenNumbers) // Seq(2, 4, 6, 8, 10)
val strings = Seq(“apple”, “banana”, “cherry”, “date”)
// 長さが5文字以上の単語を抽出
val longWords = strings.filter(_.length >= 5)
println(longWords) // Seq(“apple”, “banana”, “cherry”)
“`
3. flatMap: 変換と平坦化
flatMapメソッドは、mapと似ていますが、各要素に関数を適用した結果がSeq(または他のIterable)になり、その結果をすべて連結して一つのSeqとして返します。入れ子になったコレクションを平坦化するのによく使われます。
“`scala
val words = Seq(“hello world”, “scala programming”)
// 各単語をスペースで分割し、結果のSeq[Seq[String]]をSeq[String]に平坦化
val individualWords = words.flatMap(_.split(” “))
println(individualWords) // Seq(“hello”, “world”, “scala”, “programming”)
val numbers = Seq(1, 2, 3)
// 各数値に対して、その数値と10倍した数値をペアで生成し、平坦化
val transformed = numbers.flatMap(n => Seq(n, n * 10))
println(transformed) // Seq(1, 10, 2, 20, 3, 30)
“`
4. distinct: 重複要素の削除
distinctメソッドは、Seqから重複している要素を取り除き、新しいSeqとして返します。要素が出現した最初の順序を保持します。
“`scala
val numbersWithDuplicates = Seq(1, 3, 2, 3, 1, 4, 2, 5)
val distinctNumbers = numbersWithDuplicates.distinct
println(distinctNumbers) // Seq(1, 3, 2, 4, 5)
“`
5. reverse: 順序の反転
reverseメソッドは、Seqの要素の順序を反転させた新しいSeqを返します。
“`scala
val numbers = Seq(1, 2, 3, 4, 5)
val reversed = numbers.reverse
println(reversed) // Seq(5, 4, 3, 2, 1)
“`
6. sorted, sortBy: ソート
sorted: 要素の自然な順序(数値の大小、文字列の辞書順など)でソートした新しいSeqを返します。要素の型がOrderingトレイトを実装している必要があります。sortBy(f: A => B): 指定した関数fによって変換された値の順序でソートした新しいSeqを返します。変換された値の型BがOrderingトレイトを実装している必要があります。
“`scala
val numbers = Seq(3, 1, 4, 1, 5, 9, 2, 6)
val strings = Seq(“banana”, “apple”, “cherry”)
val sortedNumbers = numbers.sorted
println(sortedNumbers) // Seq(1, 1, 2, 3, 4, 5, 6, 9)
val sortedStrings = strings.sorted
println(sortedStrings) // Seq(“apple”, “banana”, “cherry”)
// 文字列の長さを基準にソート
val sortedByLength = strings.sortBy(_.length)
println(sortedByLength) // Seq(“apple”, “banana”, “cherry”) – この場合は長さが同じなので元の順序に依存
val strings2 = Seq(“date”, “apple”, “banana”, “cherry”)
val sortedByLength2 = strings2.sortBy(_.length)
println(sortedByLength2) // Seq(“date”, “apple”, “banana”, “cherry”) – date(4), apple(5), banana(6), cherry(6) となる
“`
7. take, drop: 要素の取り出し・スキップ
take(n: Int): Seqの先頭から指定した数nの要素を含む新しいSeqを返します。drop(n: Int): Seqの先頭から指定した数nの要素をスキップし、残りの要素を含む新しいSeqを返します。
“`scala
val numbers = Seq(10, 20, 30, 40, 50)
val firstTwo = numbers.take(2)
println(firstTwo) // Seq(10, 20)
val remaining = numbers.drop(2)
println(remaining) // Seq(30, 40, 50)
val all = numbers.take(100) // Seqの長さ以上の数を指定してもエラーにならない
println(all) // Seq(10, 20, 30, 40, 50)
val empty = numbers.drop(100) // Seqの長さ以上の数を指定
println(empty) // Seq()
“`
関連するメソッドとして、条件を満たすまで要素を取り出す/スキップするtakeWhile, dropWhileや、Seqを2つに分割するsplitAt, spanなどもあります。
8. slice: 指定範囲の抽出
slice(from: Int, until: Int)メソッドは、指定した開始インデックスfrom(含む)から終了インデックスuntil(含まない)までの要素を含む新しいSeqを返します。
“`scala
val numbers = Seq(10, 20, 30, 40, 50)
val middle = numbers.slice(1, 4) // インデックス1, 2, 3の要素を取得
println(middle) // Seq(20, 30, 40)
“`
9. zip: 別のSeqとのペアリング
zipメソッドは、Seqの各要素を、別のSeqの対応する位置の要素と組み合わせて、ペア(タプル)のSeqを生成します。長さが異なる場合は、短い方のSeqに合わせてペアが作られます。
“`scala
val numbers = Seq(1, 2, 3)
val letters = Seq(“a”, “b”, “c”, “d”)
val zipped = numbers.zip(letters)
println(zipped) // Seq((1,a), (2,b), (3,c)) // “d”は対応する要素がないので捨てられる
val numbers2 = Seq(1, 2, 3, 4, 5)
val letters2 = Seq(“a”, “b”)
val zipped2 = numbers2.zip(letters2)
println(zipped2) // Seq((1,a), (2,b)) // 3, 4, 5 は対応する要素がないので捨てられる
“`
Seqの集約操作
集約操作は、Seqの複数の要素を処理して、単一の結果値を生成する操作です。
1. reduce, fold: 要素をまとめる
reduce(op: (A, A) => A): Seqの要素を、指定した二項演算opを使って左から順に結合していきます。空のSeqに対して呼び出すと例外が発生します。fold(initial: B)(op: (B, A) => B):reduceに似ていますが、初期値initialを指定できます。最初の要素と初期値に対して演算を行い、その結果と次の要素に対して演算を行う、という流れで進みます。初期値を指定できるため、空のSeqに対しても安全に実行できます。reduceLeft,foldLeft: 左結合。reduceRight,foldRight: 右結合。通常はfoldLeft(/:) またはreduceLeftを使います。
“`scala
val numbers = Seq(1, 2, 3, 4, 5)
// reduce: 要素を合計する
val sumReduced = numbers.reduce( + ) // ((((1 + 2) + 3) + 4) + 5)
println(sumReduced) // 15
// fold: 要素を合計する (初期値 0)
val sumFolded = numbers.fold(0)( + ) // ((((0 + 1) + 2) + 3) + 4) + 5
println(sumFolded) // 15
val emptySeq: Seq[Int] = Seq.empty
// val sumEmptyReduced = emptySeq.reduce( + ) // エラー: UnsupportedOperationException: empty.reduceLeft
val sumEmptyFolded = emptySeq.fold(0)( + )
println(sumEmptyFolded) // 0 (初期値がそのまま返る)
val strings = Seq(“a”, “b”, “c”)
// 文字列を連結 (foldLeft)
val combined = strings.foldLeft(“”)( + ) // (((( “” + “a”) + “b”) + “c”)
println(combined) // “abc”
“`
2. sum, product, min, max: 数値Seqの便利な集約
要素が数値型の場合、合計、積、最小値、最大値を簡単に計算できるメソッドが用意されています。
“`scala
val numbers = Seq(1, 2, 3, 4, 5)
println(numbers.sum) // 15
println(numbers.product) // 120 (12345)
println(numbers.min) // 1
println(numbers.max) // 5
val emptySeq: Seq[Int] = Seq.empty
// println(emptySeq.sum) // エラー
// println(emptySeq.min) // エラー
``minOption
これらのメソッドも、空のSeqに対して呼び出すと例外が発生します。安全に最小値/最大値を取得したい場合は、,maxOption`を使います。
scala
println(numbers.minOption) // Some(1)
println(emptySeq.minOption) // None
3. count: 条件を満たす要素数
count(p: A => Boolean)メソッドは、指定した条件を満たす要素の数をカウントします。
“`scala
val numbers = Seq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evenCount = numbers.count(_ % 2 == 0) // 偶数の数をカウント
println(evenCount) // 5
val greaterThanFiveCount = numbers.count(_ > 5) // 5より大きい数の数をカウント
println(greaterThanFiveCount) // 5
“`
4. groupBy: 要素のグループ化
groupBy(f: A => K)メソッドは、指定した関数fを各要素に適用してキーを生成し、そのキーに基づいて要素をグループ化します。結果はMap[K, Seq[A]]として返されます。キーが存在しない要素は結果に含まれません。
“`scala
val words = Seq(“apple”, “banana”, “cherry”, “date”, “apricot”)
// 単語の最初の文字でグループ化
val groupedByFirstLetter = words.groupBy(_.head)
println(groupedByFirstLetter)
// Map(‘a’ -> Seq(“apple”, “apricot”), ‘b’ -> Seq(“banana”), ‘c’ -> Seq(“cherry”), ‘d’ -> Seq(“date”))
val numbers = Seq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// 偶数と奇数でグループ化
val groupedByParity = numbers.groupBy(_ % 2 == 0)
println(groupedByParity)
// Map(false -> Seq(1, 3, 5, 7, 9), true -> Seq(2, 4, 6, 8, 10))
“`
Seqのイテレーション
Seqの要素を順番に処理するにはいくつかの方法があります。
1. foreach: 副作用のある処理
foreach(f: A => Unit)メソッドは、Seqの各要素に対して副作用のある処理(例えば、要素を表示する、外部の状態を変更するなど)を実行する場合に使います。foreachは新しいSeqを返さず、結果はUnit(何も返さない)です。
“`scala
val numbers = Seq(1, 2, 3)
numbers.foreach(n => println(s”Processing number: $n”))
// 出力:
// Processing number: 1
// Processing number: 2
// Processing number: 3
“`
2. for 式
for 式は、コレクションのイテレーションや変換によく使われる糖衣構文(Syntactic Sugar)です。Seqに対してももちろん使用できます。
要素を順番に取り出して処理する場合 (foreachの代わり):
“`scala
val numbers = Seq(1, 2, 3)
for (n <- numbers) {
println(s”Number: $n”)
}
// 出力はforeachと同じ
“`
for 式は、map, filter, flatMapなどの組み合わせに変換されます。
例えば、mapの代わりにfor式を使う例:
“`scala
val numbers = Seq(1, 2, 3)
val doubled = for (n <- numbers) yield n * 2 // ‘yield’ を使うと結果をコレクションとして収集
println(doubled) // Seq(2, 4, 6)
``numbers.map(n => n * 2)` と同じです。
これは
filterとmapを組み合わせる例 (for式だと自然に書ける):
“`scala
val numbers = Seq(1, 2, 3, 4, 5, 6)
// 偶数だけを2倍にする
val evenDoubled = for {
n <- numbers // イテレーション
if n % 2 == 0 // フィルター(ガード条件)
} yield n * 2 // 変換
println(evenDoubled) // Seq(4, 8, 12)
``numbers.filter( % 2 == 0).map( * 2)` と同じです。
これは
flatMapを含む例:
“`scala
val sentences = Seq(“hello world”, “scala is fun”)
val words = for {
sentence <- sentences // Seq(“hello world”, “scala is fun”) をイテレート
word <- sentence.split(” “) // 各sentenceに対して split(” “) を実行。結果のSeq[String]を flatMap 的に結合
} yield word
println(words) // Seq(“hello”, “world”, “scala”, “is”, “fun”)
``sentences.flatMap(_.split(” “))` と同じです。
これは
for式は、複数のコレクションを組み合わせた処理など、複雑な変換を分かりやすく記述するのに役立ちます。
3. iterator: 低レベルなイテレーション
iteratorメソッドは、Seqの要素を順番に提供するイテレータを返します。イテレータは一度しか要素を走査できません。通常、高レベルなmap, filter, foreachなどのメソッドを使う方が推奨されますが、低レベルな制御が必要な場合や、メモリ効率を重視する場合に使われることがあります。
“`scala
val numbers = Seq(1, 2, 3)
val it = numbers.iterator
while (it.hasNext) {
val n = it.next()
println(s”Via iterator: $n”)
}
// 出力:
// Via iterator: 1
// Via iterator: 2
// Via iterator: 3
// it.next() // Iterator exhausted exception が発生
“`
Seqの実装クラスの詳細:ListとVector
ImmutableなSeqの代表的な実装であるListとVectorについて、もう少し詳しく見てみましょう。どちらを選ぶべきか、パフォーマンス特性を理解することが重要です。
scala.collection.immutable.List
Listは単方向リンクリストです。各要素は「自分の値」と「次の要素への参照」を持っています。
scala
val list = List(1, 2, 3) // head: 1, tail: List(2, 3)
// List(1, 2, 3) は内部的に 1 :: List(2, 3) と表現でき
// List(2, 3) は 2 :: List(3) と表現でき
// List(3) は 3 :: Nil と表現できる
// Nil は空のList
パフォーマンス特性:
- 先頭への要素追加 (
::,+:): O(1) – 非常に高速です。新しい要素を現在のリストの先頭に繋ぐだけで済みます。 - 先頭要素の取得 (
head,headOption): O(1) – 非常に高速です。 - 先頭以外の要素の取得 (
tail): O(1) – 非常に高速です。 - 末尾への要素追加 (
:+): O(n) – リストの末尾まで辿る必要があるため、リストの長さに比例して時間がかかります。 - 任意の位置へのアクセス (
apply(index)): O(n) – 指定されたインデックスまで先頭から順番に辿る必要があるため、インデックスに比例して時間がかかります。 - 任意の位置での挿入・削除・更新: O(n) – 基本的にO(n)かかります。
使いどころ:
- 要素の追加・削除が主に先頭で行われる場合。
- 再帰的な処理と相性が良い(headとtailでリストを分解していくパターン)。
- リストが比較的小さい場合、O(n)操作のオーバーヘッドがあまり問題にならないことがあります。
scala.collection.immutable.Vector
Vectorはブロック化されたバランスツリーです。要素は小さな固定サイズの配列(ブロック)に格納され、これらのブロックがツリー構造で連結されています。これにより、様々な位置へのアクセスや更新が効率的に行えます。
scala
val vector = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
// 内部的には、例えば32要素のブロックに分割され、それらがツリー構造で管理される
パフォーマンス特性:
- 先頭・末尾への要素追加 (
+::+): ほぼO(1)(正確にはO(log_32 N))。ツリー構造の深さは要素数の対数に比例するため、非常に効率的です。 - 任意の位置へのアクセス (
apply(index)): ほぼO(1)(正確にはO(log_32 N))。ツリー構造を辿ることで高速にアクセスできます。 - 任意の位置での更新: ほぼO(1)(正確にはO(log_32 N))。該当ブロックをコピーしてツリー構造を再構築しますが、効率的な構造になっています。
map,filterなどの変換操作: これらの操作も効率的に実行できます。
使いどころ:
- 要素の追加・削除が先頭、末尾、または中間位置で頻繁に行われる場合。
- 任意の位置へのランダムアクセスが多い場合。
- 大規模なデータを扱う場合(メモリ効率やキャッシュ効率も考慮されています)。
結論として、特別な理由(例えば、再帰処理でtailを頻繁に使うなど)がない限り、ScalaのImmutableな順序付きコレクションとしてはVectorを選択するのが無難でパフォーマンスが良いことが多いです。 特に大規模データの場合や、ランダムアクセス・更新が必要な場合は、Vectorが推奨されます。
immutable vs mutable Seq
ScalaのデフォルトはImmutableなコレクションですが、パフォーマンスがクリティカルな場合や特定のパターンではMutableなコレクションを使うこともあります。SeqにもMutableなバージョンが存在します。
scala.collection.mutable.Seq[A]
代表的なMutableなSeqの実装クラスはArrayBufferです。
scala.collection.mutable.ArrayBuffer: 可変長の配列です。要素の追加や削除、更新が効率的に行えます。
“`scala
import scala.collection.mutable
val mutableSeq: mutable.Seq[Int] = mutable.ArrayBuffer(1, 2, 3)
println(mutableSeq) // ArrayBuffer(1, 2, 3)
mutableSeq += 4 // 要素を末尾に追加
println(mutableSeq) // ArrayBuffer(1, 2, 3, 4)
mutableSeq.prepend(0) // 要素を先頭に追加
println(mutableSeq) // ArrayBuffer(0, 1, 2, 3, 4)
mutableSeq.remove(2) // インデックス2の要素を削除
println(mutableSeq) // ArrayBuffer(0, 1, 3, 4)
mutableSeq(1) = 10 // インデックス1の要素を更新
println(mutableSeq) // ArrayBuffer(0, 10, 3, 4)
“`
MutableなSeqは、上記のようにメソッド呼び出しによって自身の内容が変更されます。
どちらを使うべきか?
前述の通り、ScalaではImmutableなコレクションを優先するのが標準的なプラクティスです。
Immutableの利点:
* 並行処理で安全
* プログラムの挙動が予測しやすい
* 変更履歴を追いやすい(デバッグ容易性)
Mutableを使う場合の考慮事項:
* パフォーマンス: 大量の要素を繰り返し追加・削除するような場合、Mutableなコレクションの方が効率的な場合があります。Immutableなコレクションは操作ごとに新しいコレクションを生成するため、そのオーバーヘッドが無視できないことがあります。
* ローカルスコープ: 関数やメソッドの内部だけで使用され、外部に公開されないMutableなコレクションであれば、安全性の懸念が少なく、パフォーマンスのために選択肢となり得ます。
* Javaとの連携: JavaのライブラリはMutableなコレクションを扱うことが多いため、ScalaコードからJavaライブラリにコレクションを渡す場合などにMutableなコレクションが使われることがあります。
しかし、多くの場合はImmutableなコレクションで十分なパフォーマンスが得られます。パフォーマンスが問題になった場合に初めてMutableなコレクションを検討するのが良いでしょう。
実践的なSeqの使い方とヒント
1. 適切なSeq実装クラスの選択
- デフォルトで
Seq(...)またはVector(...)を使うのが無難です。 - 主に先頭操作(追加・削除)が多い、または再帰処理でリストの分割を多用する場合は
Listを検討します。 - ランダムアクセス、末尾操作、中間更新が多い場合、または大規模データの場合は
Vectorを選択します。 - 配列として扱うのが最も自然な場合(固定長など)や、Javaコードとの連携が多い場合は
ArraySeqも選択肢に入ります。
2. 高階関数(map, filter, flatMapなど)の活用
これらのメソッドは、Seqを宣言的に、簡潔に関数型スタイルで操作するための強力なツールです。ループ(while, for with var)を使うよりも、これらのメソッドを使った方が、コードの意図が明確になり、バグも入りにくくなります。
“`scala
// BAD: ループで処理
val numbers = Seq(1, 2, 3, 4, 5)
var doubled = Seq.empty[Int]
for (n <- numbers) {
doubled :+= n * 2 // Immutable Seqへの追加は遅い場合があるし、mutable varを使うのは関数型らしくない
}
println(doubled)
// GOOD: mapを使う
val numbers = Seq(1, 2, 3, 4, 5)
val doubled = numbers.map(_ * 2)
println(doubled)
“`
3. パターンマッチングとの組み合わせ
特にListを扱う場合、パターンマッチングは非常に強力です。リストの構造(空リストか、先頭要素とそれ以外かなど)をパターンで表現し、それぞれのケースに応じた処理を書くことができます。
“`scala
def processList(list: List[Int]): Int = list match {
case Nil => 0 // 空のリストの場合
case head :: tail => head + processList(tail) // 先頭要素::残りのリスト の場合
}
println(processList(List(1, 2, 3))) // 6
println(processList(List.empty)) // 0
``fold
これはリストの要素を合計する再帰関数ですが、やsum`を使う方がより簡潔で効率的です。しかし、パターンマッチングがリスト処理の強力な手段であることを示しています。
4. ビュー (view) を使った遅延評価
viewメソッドは、コレクション操作をすぐに実行せず、イテレーションが開始されるまで遅延させるためのものです。複数の変換操作をチェーンする場合に、中間的なコレクションの生成を抑え、メモリ効率やパフォーマンスを改善できることがあります。
“`scala
val numbers = (1 to 1000000).toSeq // 大きなSeq
// 通常の操作: map, filter, take のたびに新しいSeqが生成される可能性がある
val resultImmediate = numbers
.map( * 2)
.filter( % 3 == 0)
.take(10)
// Viewを使った操作: 遅延評価される。take(10)が必要な要素だけを処理する
val resultLazy = numbers.view
.map( * 2)
.filter( % 3 == 0)
.take(10)
.toSeq // 結果が必要になった時点で評価を実行し、Seqに戻す
println(resultImmediate)
println(resultLazy)
``viewを使うと、上記の例ではtake(10)で最終的に必要な要素は10個だけなので、100万個の要素全てに対してmapやfilterを実行する必要がなくなります。ただし、viewは一度だけイテレートできる(または効率的なランダムアクセスができない)場合があります。最終的にtoSeqやtoList`などで具体的なコレクションに戻す必要があります。
5. パフォーマンスに関する考慮事項
- ImmutableなListの末尾追加 (
:+) やインデックスアクセス (apply(index)) はO(n)で遅いことを意識する。これらの操作が多い場合はVectorを使う。 - ImmutableなVectorの多くの操作はほぼO(1)またはO(log N)で高速だが、大規模データでの要素の更新はコピーが発生するため、きわめて頻繁に行う場合はMutableな
ArrayBufferの方が有利な場合がある。 - 繰り返し同じMutableなコレクションを構築・変更する場合、Mutableな
ArrayBufferやListBufferを使う方が効率的だが、副作用や並行性のリスクを理解して使う。 foreachは要素の表示など副作用のために使う。新しいコレクションを生成したい場合はmap,filter,flatMapなどを使う。
6. Option や Either/Try との組み合わせ
findなどのメソッドは結果をOptionで返すため、要素が見つからなかった場合の処理を安全に行えます。
“`scala
val numbers = Seq(10, 20, 30)
val found = numbers.find( > 25) // Some(30)
val notFound = numbers.find( > 100) // None
found match {
case Some(value) => println(s”Found: $value”)
case None => println(“Not found”)
}
“`
これにより、nullチェックや例外処理を減らし、より堅牢なコードが書けます。
まとめ
この記事では、ScalaプログラミングにおけるSeqコレクションの使い方について、基本から応用まで詳しく解説しました。
Seqは、要素が順序付けられ、インデックスアクセス可能なImmutable(デフォルト)なコレクションを表すトレイトです。- Scalaのコレクション階層において
Iterableの下に位置し、ListやVectorなどの具体的な実装クラスがあります。 - Immutableなコレクションは、並行処理の安全性やプログラムの予測可能性を高める上で非常に重要であり、Scalaでは推奨されます。
Seqの生成方法は、ファクトリメソッドSeq(...)、特定のクラスのコンストラクタ、範囲からの変換、既存コレクションからの変換などがあります。- 要素へのアクセス (
apply,head,tail,last,init)、要素数の取得 (length,size)、空判定 (isEmpty,nonEmpty)、存在チェック (contains,exists,forall)、検索 (indexOf,find)、連結 (++)、要素の追加 (+:,:+) など、基本的な操作を学びました。 map,filter,flatMap,distinct,reverse,sorted,sortBy,take,drop,slice,zipなど、Immutableなコレクションの強力な変換操作を多数紹介しました。これらの操作は常に新しいSeqを返します。reduce,fold,sum,product,min,max,count,groupByなど、Seqの要素をまとめて単一の結果を生成する集約操作を学びました。foreachやfor式を使ったイテレーション方法も紹介しました。ListとVectorという代表的なImmutableSeq実装クラスのパフォーマンス特性と使い分けについて解説しました。一般的にはVectorがより汎用的で効率的です。- ImmutableとMutableの選択基準についても触れました。
- 最後に、高階関数の活用、パターンマッチング、ビュー、パフォーマンス考慮、Optionとの組み合わせなど、実践的なヒントをいくつか紹介しました。
Seqとその豊富な操作は、Scalaでデータを扱う上での基盤となります。最初は多くのメソッドに戸惑うかもしれませんが、実際にコードを書きながら少しずつ慣れていくことが大切です。特にmap, filter, flatMapなどの高階関数は、関数型プログラミングのスタイルを身につける上で非常に重要なので、積極的に使ってみてください。
この記事が、ScalaにおけるSeqの理解を深め、データ処理のスキル向上の一助となれば幸いです。
次に学ぶこととしては、順序付けられていない要素の集合であるSetや、キーと値のペアを扱うMap、そしてOptionalな値を安全に扱うOptionなどがあります。これらもScalaプログラミングでは非常によく使われるコレクションやデータ構造です。
Scalaのコレクションライブラリは奥深く、ここに書ききれなかった機能もたくさんあります。さらに詳しく知りたい場合は、Scala公式ドキュメントのコレクションに関するページを参照することをおすすめします。
Happy Scala coding!