M.Hiroi's Home Page

お気楽 Scala プログラミング入門

列 (Seq)


Copyright (C) 2014-2024 Makoto Hiroi
All rights reserved.

はじめに

今回は「列 (Seq)」について説明します。Seq はシーケンス (sequence) の略で、要素を順番に並べたコレクションのことを「列」といいます。Scala の場合、列は Seq トレイトとして定義されていて、Seq を継承 (Mix-in) しているデータ型であれば、列として統一的に扱うことができます。

●リストと列の違い

列の代表的なデータがリスト (List) ですが、リストのコンス演算子 (::) と連結演算子 (:::) は Seq トレイトにはありません。列の先頭に要素を追加するには演算子 +: を使い、列の末尾に要素を追加するには演算子 :+ を使います。列を連結する場合は演算子 ++ を使います。もちろん、これらの演算子はリストにも適用することができます。

簡単な例を示しましょう。

scala> val a = List(1, 2, 3, 4, 5)
val a: List[Int] = List(1, 2, 3, 4, 5)

scala> 0 +: a
val res0: List[Int] = List(0, 1, 2, 3, 4, 5)

scala> a :+ 6
val res1: List[Int] = List(1, 2, 3, 4, 5, 6)

scala> a ++ List(6, 7, 8, 9)
val res2: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)

●不変 (immutable) な列

不変な列は、リスト以外ではおもに String, Range, Vector などがあります。String と Range は今までもよく使ってきましたが、Vector (ベクタ、ベクトル) は初めでなので簡単に説明しましょう。

リストの要素を参照する場合、セルを順番にたどっていく必要があるので、後ろの要素ほど時間がかかるようになります。Vector はこれを改良したデータ構造で、要素の参照はほぼ一定時間で行うことができるようになっています。他のプログラミング言語、たとえば Scheme では一次元配列のことをベクタと呼びますが、Scala のベクタは配列ではありません。したがって、要素の参照は配列よりも時間がかかります。ご注意ください。

簡単な例を示しましょう。

scala> val b = Vector(1, 2, 3, 4, 5)
val b: Vector[Int] = Vector(1, 2, 3, 4, 5)

scala> b(0)
val res3: Int = 1

scala> b(4)
val res4: Int = 5

scala> 0 +: b
val res5: Vector[Int] = Vector(0, 1, 2, 3, 4, 5)

scala> b :+ 6
val res6: Vector[Int] = Vector(1, 2, 3, 4, 5, 6)

scala> b ++ List(6, 7, 8, 9)
val res7: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9)

scala> a ++ b
val res8: List[Int] = List(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)

もちろん、String にも Seq トレイトのメソッドを適用することができます。

scala> val c = "abcdefg"
val c: String = abcdefg

scala> c(0)
val res9: Char = a

scala> c(6)
val res10: Char = g

scala> 'A' +: c
val res11: String = Aabcdefg

scala> c :+ 'Z'
val res12: String = abcdefgZ

●列の生成

列の生成はコンパニオンオブジェクトの apply メソッドだけではなく、次に示すメソッドもコンパニオンオブジェクトに用意されています。

empty                         : 空の列を生成する。
concat(col1, col2, ...)       : 他のコレクションから列を生成する。
fill(大きさ)(値)              : 列を指定した値で満たす。
tabulate(大きさ)(関数)        : 添字に関数を適用して、返り値を列に格納して返す。
iterate(初期値, 大きさ)(関数) : 前の要素に関数を適用して、その返り値を列に格納して返す。
range(開始, 終了, [増分])     : 指定した範囲の数列を生成する。

簡単な例を示しましょう。

scala> List.empty[Int]
val res13: List[Int] = List()

scala> Vector.empty[Int]
val res14: Vector[Int] = Vector()

scala> List.concat(Vector(1, 2, 3, 4), List(5, 6, 7, 8))
val res15: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8)

scala> Vector.concat(Vector(1, 2, 3, 4), List(5, 6, 7, 8))
val res16: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8)

scala> List.fill(5)(0)
val res17: List[Int] = List(0, 0, 0, 0, 0)

scala> Vector.fill(5)(0)
val res18: Vector[Int] = Vector(0, 0, 0, 0, 0)

scala> List.tabulate(5)(x => x * x)
val res19: List[Int] = List(0, 1, 4, 9, 16)

scala> Vector.tabulate(5)(x => x * x)
val res20: Vector[Int] = Vector(0, 1, 4, 9, 16)

scala> List.iterate(1, 5)(x => x * 2)
val res21: List[Int] = List(1, 2, 4, 8, 16)

scala> Vector.iterate(1, 5)(x => x * 2)
val res22: Vector[Int] = Vector(1, 2, 4, 8, 16)

scala> List.range(1, 8)
val res23: List[Int] = List(1, 2, 3, 4, 5, 6, 7)

scala> Vector.range(1, 8)
val res24: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7)

なお、これらのメソッドは配列 (Array) にも用意されています。

scala> Array.empty[Int]
val res25: Array[Int] = Array()

scala> Array.concat(Array(1, 2, 3), Array(4, 5, 6))
val res26: Array[Int] = Array(1, 2, 3, 4, 5, 6)

scala> Array.fill(10)(0)
val res27: Array[Int] = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

scala> Array.tabulate(5)(x => x * x)
val res28: Array[Int] = Array(0, 1, 4, 9, 16)

scala> Array.iterate(1, 5)(x => x * 2)
val res29: Array[Int] = Array(1, 2, 4, 8, 16)

scala> Array.range(1, 10)
val res30: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9)

●可変 (mutable) な列

可変な列はおもに ArrayBuffer, ListBuffer, StringBuilder があります。ArrayBuffer は要素を格納するバッファに配列を使っていて、いわゆる「可変長配列」として使用することができます。ArrayBuffer はバッファの末尾に対する操作が得意です。ListBuffer はバッファに連結リストを使っていて、バッファの先頭に対する操作が得意です。StringBuilder は文字列を組み立てるために使います。

可変な列 seq は seq(n) = x で要素を更新できますが、このほかにも要素の追加や削除を行うメソッドが用意されています。

seq += x              : x を末尾に追加する
seq ++= xs            : xs の要素をすべて末尾に追加する
x +=: seq             : x を先頭に追加する
xs +=: seq            : xs の要素をすべて先頭に追加する
seq insert (i, x)     : x を i 番目に挿入する
seq insertAll (i, xs) : xs の要素をすべて i 番目に挿入する
seq -= x              : 要素 x を削除する
seq remove i          : i 番目の要素を削除する
seq remove (i, n)     : i 番目から n 個の要素を削除する
seq trimStart n       : 先頭から n  個の要素を削除する
seq trimEnd n         : 末尾から n  個の要素を削除する

簡単な例を示しましょう。

scala> import scala.collection.mutable.{ArrayBuffer, ListBuffer}

scala> val a = new ArrayBuffer[Int]
val a: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer()

scala> for (i <- 1 to 5) a += i

scala> a
val res1: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(1, 2, 3, 4, 5)

scala> a(0)
val res2: Int = 1

scala> a(4)
val res3: Int = 5

scala> a(0) = 100

scala> a
val res5: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(100, 2, 3, 4, 5)

scala> val b = new ListBuffer[Int]
val b: scala.collection.mutable.ListBuffer[Int] = ListBuffer()

scala> for (i <- 1 to 5) i +=: b

scala> b
val res10: scala.collection.mutable.ListBuffer[Int] = ListBuffer(5, 4, 3, 2, 1)

scala> b(0)
val res11: Int = 5

scala> b(4)
val res12: Int = 1

scala> b(4) = 100

scala> b
val res14: scala.collection.mutable.ListBuffer[Int] = ListBuffer(5, 4, 3, 2, 100)

scala> a ++= b
val res15: a.type = ArrayBuffer(100, 2, 3, 4, 5, 4, 3, 2, 100)

scala> a ++=: b
val res16: b.type = ListBuffer(100, 2, 3, 4, 5, 4, 3, 2, 100, 5, 4, 3, 2, 100)

scala> a -= 100
val res17: a.type = ArrayBuffer(2, 3, 4, 5, 4, 3, 2, 100)

scala> a.toArray
val res18: Array[Int] = Array(2, 3, 4, 5, 4, 3, 2, 100)

scala> b -= 100
val res19: b.type = ListBuffer(2, 3, 4, 5, 4, 3, 2, 100, 5, 4, 3, 2, 100)

scala> b -= 100
val res20: b.type = ListBuffer(2, 3, 4, 5, 4, 3, 2, 5, 4, 3, 2, 100)

scala> b.toList
val res21: List[Int] = List(2, 3, 4, 5, 4, 3, 2, 5, 4, 3, 2, 100)

StringBuilder は文字列を組み立てるときに使うと便利です。簡単な例を示しましょう。

scala> val c = new StringBuilder
val c: StringBuilder =

scala> c += 'a'
val res22: c.type = a

scala> c ++= "bcdefg"
val res23: c.type = abcdefg

scala> c.toString
val res24: String = abcdefg

StringBuilder は標準でインポートされているので、簡単に使用することができます。

●ArraySeq

配列は Seq トレイトを継承していませんが、メソッド toIndexedSeq で ArraySeq というコレクションに変換することができます。ArraySeq は Seq トレイトを継承していて、配列を Seq 型のように使うことができます。ArraySeq は mutable と immutable がありますが、toIndexedSeq が返す ArraySeq は immutable のほうです。次の例を見てください。

scala>  val a = Array(1, 2, 3, 4, 5)
val a: Array[Int] = Array(1, 2, 3, 4, 5)

scala> val b = a.toIndexedSeq
val b: IndexedSeq[Int] = ArraySeq(1, 2, 3, 4, 5)

scala> b.toArray
val res0: Array[Int] = Array(1, 2, 3, 4, 5)

ArraySeq は toArray メソッドで配列に戻すことも簡単にできます。

Scala の配列は演算子 ==, != で等値の判定はできませんが、ArraySeq は演算子 ==, != で等値を判定することができます。

scala> val c = Array(1, 2, 3, 4, 5)
val c: Array[Int] = Array(1, 2, 3, 4, 5)

scala> a == c
val res1: Boolean = false

scala> val d = c.toIndexedSeq
val d: IndexedSeq[Int] = ArraySeq(1, 2, 3, 4, 5)

scala> b == d
val res2: Boolean = true

実をいうと、配列は列と同じ演算がサポートされています。次の例を見てください

scala> a
val res5: Array[Int] = Array(1, 2, 3, 4, 5)

scala> a :+ 5
val res6: Array[Int] = Array(1, 2, 3, 4, 5, 5)

scala>  0 +: a
val res7: Array[Int] = Array(0, 1, 2, 3, 4, 5)

scala> a ++ Array(6,7,8,9,10)
val res8: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

COLLECTIONS - Scala Documentation の「配列」によると、内部では暗黙の型変換により処理が行われているそうです。初心者にとって、Scala の暗黙の型変換はやっぱり難しいですね。

●列のパターンマッチング

リストと同様に列でもパターンマッチングを行うことができます。簡単な例を示しましょう。なお、Scala 3 では warning が表示されることがありますが、ここでは無視してください。

scala> val (x +: xs) = Vector(1, 2, 3, 4, 5)
x: Int = 1
xs: Vector[Int] = Vector(2, 3, 4, 5)

scala> val (x +: xs) = List(1, 2, 3, 4, 5)
x: Int = 1
xs: List[Int] = List(2, 3, 4, 5)

scala> val (ys :+ y) = Vector(1, 2, 3, 4, 5)
ys: Vector[Int] = Vector(1, 2, 3, 4)
y: Int = 5

scala> val (ys :+ y) = List(1, 2, 3, 4, 5)
ys: List[Int] = List(1, 2, 3, 4)
y: Int = 5

scala> val (a +: b +: c +: d) = Vector(1, 2, 3, 4, 5)
a: Int = 1
b: Int = 2
c: Int = 3
d: Vector[Int] = Vector(4, 5)

scala> val (a +: b +: c +: d) = List(1, 2, 3, 4, 5)
a: Int = 1
b: Int = 2
c: Int = 3
d: List[Int] = List(4, 5)

演算子 +: はコンス演算子 (::) と同じように使うことができます。演算子 :+ を使うと、最後尾の要素を簡単に取り出すことができます。

次のように、Seq(...) とマッチングさせることもできます。

scala> val Seq(a, b, c) = Vector(1, 2, 3)
a: Int = 1
b: Int = 2
c: Int = 3

scala> val Seq(a, b, c) = List(1, 2, 3)
a: Int = 1
b: Int = 2
c: Int = 3

scala> val Seq(a, b, c, _*) = Vector(1, 2, 3, 4, 5)
a: Int = 1
b: Int = 2
c: Int = 3

scala> val Seq(a, b, c, _*) = List(1, 2, 3, 4, 5)
a: Int = 1
b: Int = 2
c: Int = 3

_* は 0 個以上の任意の要素とマッチングします。

記号 @ を使うと変数とパターンを同時に設定することができます。これを「as パターン」といいます。

変数名 @ パターン

たとえば、xs @ (y :: ys) と List(1, 2, 3) をマッチングさせると、次のようになります。

xs => List(1, 2, 3)
y  => 1
ys => List(2, 3)

もうひとつ簡単な例を示しましょう。

scala> val Seq(a, b, c, d @ _*) = List(1, 2, 3, 4, 5)
a: Int = 1
b: Int = 2
c: Int = 3
d: Seq[Int] = List(4, 5)

パターン _* とマッチングした値は変数 d にセットされます。

●列の操作メソッド

列からデータを探索する場合、今まで用いてきたメソッド contains, indexOf, find, count はそのまま列に使用することができます。

scala> import scala.collection.mutable.{ArrayBuffer}

scala> val a = Vector(1,2,3,4,5,6,7,8)
val a: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8)

scala> val b = ArrayBuffer(10, 20, 30, 40, 50)
val b: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(10, 20, 30, 40, 50)

scala> a contains 5
val res0: Boolean = true

scala> a contains 50
val res1: Boolean = false

scala> b contains 5
val res2: Boolean = false

scala> b contains 50
val res3: Boolean = true

scala> a indexOf 5
val res4: Int = 4

scala> a indexOf 50
val res5: Int = -1

scala> b indexOf 5
val res6: Int = -1

scala> b indexOf 50
val res7: Int = 4

scala> a find (_ % 2 == 0)
val res8: Option[Int] = Some(2)

scala> b find (_ % 2 == 0)
val res9: Option[Int] = Some(10)

scala> a count (_ % 2 == 0)
val res10: Int = 4

scala> b count (_ % 2 == 0)
val res11: Int = 5

メソッド reverse で列を反転することもできます。

scala> a.reverse
val res12: Vector[Int] = Vector(8, 7, 6, 5, 4, 3, 2, 1)

scala> a
val res13: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8)

scala> b.reverse
val res14: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(50, 40, 30, 20, 10)

scala> b
val res15: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(10, 20, 30, 40, 50)

元の列を反転した新しい列が生成されます。

高階関数 map, filter, foldLeft, FoldRight, foreach も使用することができます。

scala> a.map(x => x * x)
val res16: Vector[Int] = Vector(1, 4, 9, 16, 25, 36, 49, 64)

scala> b.map(x => x * x)
val res17: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(100, 400, 900, 1600, 2500)

scala> a.filter(_ % 2 == 0)
val res18: Vector[Int] = Vector(2, 4, 6, 8)

scala> b.filter(_ % 20 == 0)
val res19: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(20, 40)

scala> a.foldLeft(0)(_ + _)
val res20: Int = 36

scala> b.foldLeft(0)(_ + _)
val res21: Int = 150

scala> a foreach println
1
2
3
4
5
6
7
8

scala> b foreach println
10
20
30
40
50

リストのように、head で先頭要素を、tail で先頭要素を取り除いた列を、last で末尾の要素を取得することができます。

scala> a.head
val res22: Int = 1

scala> b.head
val res23: Int = 10

scala> a.tail
val res24: Vector[Int] = Vector(2, 3, 4, 5, 6, 7, 8)

scala> b.tail
val res25: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(20, 30, 40, 50)

scala> a.last
val res26: Int = 8

scala> b.last
val res27: Int = 50

リストの場合、head と tail は高速に動作しますが、他のデータ型がリストと同じように高速に動作するわけではありません。ご注意くださいませ。

●列のソート

列をソートするにはメソッド sortBy, sortWith, sorted を使います。

def sortBy[B](f: (A) => B)(implicit ord: math.Ordering[B]): Seq[A]
def sortWith(lt: (A, A) => Boolean): Seq[A]
def sorted[B >: A](implicit ord: math.Ordering[B]): Seq[A]

sortWith は引数 lt に比較関数を渡して、それを使って列をソートします。sortBy は列の要素に引数 f の関数を適用し、その結果に対してソートを行います。これらのソートはすべて新しい列を返すことに注意してください。

簡単な例を示しましょう。

scala> List(5, 6, 4, 7, 3, 8, 2, 9, 1).sorted
val res0: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)

scala> Vector(5, 6, 4, 7, 3, 8, 2, 9, 1).sorted
val res1: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9)

scala> import scala.collection.mutable.{ArrayBuffer}

scala> val a = ArrayBuffer(5, 6, 4, 7, 3, 8, 2, 9, 1)
val a: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(5, 6, 4, 7, 3, 8, 2, 9, 1)

scala> a.sorted
val res2: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(1, 2, 3, 4, 5, 6, 7, 8, 9)

scala> a
val res3: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(5, 6, 4, 7, 3, 8, 2, 9, 1)

scala> val b = Array(5, 6, 4, 7, 3, 8, 2, 9, 1)
val b: Array[Int] = Array(5, 6, 4, 7, 3, 8, 2, 9, 1)

scala> b.sorted
val res4: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9)

scala> b
val res5: Array[Int] = Array(5, 6, 4, 7, 3, 8, 2, 9, 1)

mutable な列や配列でも、sorted でソートすると新しい列や配列が生成されます。

要素の一部の値を基準にソートしたい場合は sortBy を使うと簡単です。たとえば、列の要素がタプルの場合、要素を基準にして次のようにソートすることができます。

scala> List(("foo", 30), ("oops", 10), ("baz", 20), ("bar", 40)).sortBy(_._1)
val res6: List[(String, Int)] = List((bar,40), (baz,20), (foo,30), (oops,10))

scala> List(("foo", 30), ("oops", 10), ("baz", 20), ("bar", 40)).sortBy(_._2)
val res7: List[(String, Int)] = List((oops,10), (baz,20), (foo,30), (bar,40))

sortWith に渡す比較関数 lt(x, y) は、x が y よりも小さい場合に真を返すと、列を昇順にソートします。

scala> List(5,6,4,7,3,8,2,9,1).sortWith(_ < _)
val res8: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)

scala> List(5,6,4,7,3,8,2,9,1).sortWith(_ > _)
val res9: List[Int] = List(9, 8, 7, 6, 5, 4, 3, 2, 1)

scala> List(("foo", 30), ("oops", 10), ("baz", 20), ("bar", 40)).sortWith(_._1 < _._1)
val res10: List[(String, Int)] = List((bar,40), (baz,20), (foo,30), (oops,10))

scala> List(("foo", 30), ("oops", 10), ("baz", 20), ("bar", 40)).sortWith(_._1 > _._1)
val res11: List[(String, Int)] = List((oops,10), (foo,30), (baz,20), (bar,40))

このほかにも便利なメソッドが多数用意されています。詳細は Scala のリファレンスをお読みください。

●Ordering

Ordering はトレイトで、仮想メソッド compare を定義すると、Ordering のメソッドを使用することができます。

abstract def compare(x: T, y: T): Int  // 大小関係を符号 (負, 0, 正) で返す
def equiv(x: T, y: T): Boolean   // x == y
def gt(x: T, y: T)   : Boolean   // x > y
def gteq(x: T, y: T) : Boolean   // x >= y
def lt(x: T, y: T)   : Boolean   // x < y
def lteq(x: T, y: T) : Boolean   // x <= y

sorted や sortBy に Ordering を渡す場合は、無名のクラスを使うと簡単です。次の例を見てください。

scala> List(5, 6, 4, 7, 3, 8, 2, 9, 1).sorted(new Ordering[Int] {
     | def compare(x: Int, y:Int): Int = (x - y).sign
     | })
val res12: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)

scala> List(5, 6, 4, 7, 3, 8, 2, 9, 1).sorted(new Ordering[Int] {
     | def compare(x: Int, y:Int): Int = (y - x).sign
     | })
val res13: List[Int] = List(9, 8, 7, 6, 5, 4, 3, 2, 1)

scala> List(5, 6, 4, 7, 3, 8, 2, 9, 1).sorted(new Ordering[Int] {
     | def compare(x: Int, y:Int): Int = y - x
     | })
val res14: List[Int] = List(9, 8, 7, 6, 5, 4, 3, 2, 1)

以前のバージョンでは、整数の符号 (-1, 0, 1) を求めるときメソッド signum を使いましたが、ver 2.13.0 から非推奨になりました。かわりにメソッド sign を使ってください。なお、compare は sign で -1, 0, 1 に変換しなくても、負, 0, 正 を返すだけで動作するようです。

●参考 URL

  1. COLLECTIONS - Scala Documentation

初版 2014 年 9 月 20 日
改訂 2024 年 12 月 23 日