Sequences
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10 (3.10.0, released 2026-10-12) and every example was compiled and run with Scala 3.10.0 through Scala CLI on JDK 26 (checked on 2026-10-15). Sequences are part of the 2.13 collections library that Scala 3 shares, so nothing here changed on the 3.9 LTS (3.9.0, 2026-09-03) or 3.3 LTS (3.3.8) lines, apart from one compiler message listed in Scala 3.9 LTS versus 3.10.
A sequence is an Iterable whose elements have a fixed position: xs(0) is the first element and the elements keep the
order in which they were put. The position makes some questions cheap (what is the third element?) and others expensive
(insert an element at index 1000), and the many sequence classes exist because each one makes a different trade-off. The
overall map is on Collections Overview; the sets and maps are on
Sets & Maps; map, filter, folds and friends are on
Collection Operations; the cost tables are discussed on
Collection Performance & Equality. The examples use
the trailhead scenario, where a hike is a sequence of stops. The Java counterparts are in
the Java Collections Framework.
Which sequence
| Class | Package and kind | Use it for |
|---|---|---|
|
|
Recursion, pattern matching with |
|
|
The general-purpose immutable sequence: effectively constant time indexing, update, prepend and append. |
|
|
Arithmetic progressions ( |
|
immutable and mutable, indexed |
A |
|
immutable, linear |
FIFO with amortised constant time |
|
|
A growable array: append in amortised constant time, index in constant time. |
|
|
Building a |
|
|
Fast add and remove at both ends; use it for queues, deques and stacks. |
|
JVM array |
Fixed size, primitive speed, Java interop. |
|
Scala 3, opaque type over an array |
A read-only view of an array in the type system. |
|
|
Building a |
The rest of the page takes them one by one. The cost numbers come from the performance table in the official 2.13 guide, with C for constant, eC for effectively constant, aC for amortised constant and L for linear in the size:
| Class | head | tail | apply | update | prepend | append |
|---|---|---|---|---|---|---|
|
C |
C |
L |
L |
C |
L |
|
eC |
eC |
eC |
eC |
eC |
eC |
|
C |
L |
C |
L |
L |
L |
|
aC |
aC |
L |
L |
C |
C |
|
C |
C |
C |
- |
- |
- |
|
C |
L |
C |
C |
L |
aC |
|
C |
L |
L |
L |
C |
C |
|
C |
L |
C |
C |
aC |
aC |
|
C |
L |
C |
C |
- |
- |
List
List is an immutable singly linked list built from two cases: Nil, the empty list, and :: ("cons"), a cell holding a
head and a tail that is itself a List. Because a cell never changes, any list can share its tail with other lists:
x :: xs allocates a single new cell and reuses xs in full. That makes prepending, head and tail constant time, and it
makes indexing, length, last and appending linear.
//> using scala 3.10.0
package trailhead
@main def lists(): Unit =
val stops = List("Hut", "Ridge", "Pass")
println(stops.head)
println(stops.tail)
println("Start" :: stops)
println(stops :+ "Summit")
println(List(0) ::: stops.map(_.length))
println(stops.last)
println(stops(1))
println(Nil)
println(1 :: 2 :: Nil)
stops match
case first :: rest => println(s"$first then $rest")
case Nil => println("empty")
val shared = "A" :: stops
val other = "B" :: stops
println(shared.tail eq other.tail)
println(List.fill(3)("x"))
println(List.range(0, 10, 3))
println(stops.reverse)
println(stops.length)
Hut
List(Ridge, Pass)
List(Start, Hut, Ridge, Pass)
List(Hut, Ridge, Pass, Summit)
List(0, 3, 5, 4)
Pass
Ridge
List()
List(1, 2)
Hut then List(Ridge, Pass)
true
List(x, x, x)
List(0, 3, 6, 9)
List(Pass, Ridge, Hut)
3
Reading the output: :: and ::: end with a colon, so they are right-associative and are methods of the right operand
("Start" :: stops is stops.::("Start")); 1 :: 2 :: Nil is Nil.::(2).::(1). The line printing true shows that two lists
built with "A" :: stops and "B" :: stops share the very same tail object (eq is reference equality). :+ appends but
copies the whole list, so a loop that appends to a List is quadratic; prepend and reverse at the end instead, or use a
ListBuffer.
Source: Scala 3 Book: List and Collections 2.13: Lists
The natural fit for List is recursion over its front, with the pattern
case first :: rest, covered on
Pattern Matching and
Recursion & Tail Calls. It is a LinearSeq: the cost of
xs(i) grows with i, so avoid index loops over a List.
Vector
Vector is the immutable sequence for everything that is not "recursion over the front". It is a wide, shallow tree (a
trie with branching factor 32), so all of apply, updated, :+ and +: take effectively constant time, and an "update"
copies only a handful of small arrays and shares the rest.
//> using scala 3.10.0
package trailhead
@main def vectors(): Unit =
val v = Vector(10, 20, 30, 40)
println(v(2))
println(v.updated(1, 99))
println(v :+ 50)
println(0 +: v)
println(v)
println(v.take(2))
println(v.last)
val big = Vector.tabulate(1000)(identity)
println(big.updated(500, -1)(500))
println(big(500))
println(Vector.empty[Int])
println(v.getClass.getSimpleName)
println(big.getClass.getSimpleName)
30
Vector(10, 99, 30, 40)
Vector(10, 20, 30, 40, 50)
Vector(0, 10, 20, 30, 40)
Vector(10, 20, 30, 40)
Vector(10, 20)
40
-1
500
Vector()
Vector1
Vector2
The class names at the end (Vector1, Vector2) are the internal depth: a Vector of up to 32 elements is a single array,
up to 1024 elements two levels, and so on up to six levels. The updated vector big.updated(500, -1) has a different element
at index 500 while big itself is unchanged.
Source: Scala 3 Book: Vector and Collections 2.13: Vectors
When in doubt about which immutable sequence to use, use Vector. Use List when you consume the sequence from the front,
and Vector when you index into it or append to it. Seq(...) builds a List by default; if you want a Vector write
Vector(...) or declare the type IndexedSeq.
ArrayBuffer and ListBuffer
When you build a sequence in a loop, or need in-place updates, use a mutable buffer. A Buffer is a growable Seq with +=
and ++= to append, prepend and insert to add elsewhere, -= and remove to delete, and in-place variants of the
transformations (filterInPlace, mapInPlace, dropInPlace).
//> using scala 3.10.0
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
@main def buffers(): Unit =
val buf = ArrayBuffer("a", "b")
buf += "c"
buf ++= List("d", "e")
println(buf)
buf.prepend("z")
buf.insert(2, "m")
println(buf)
buf -= "a"
buf.remove(0)
println(buf)
buf(0) = "Q"
println(buf)
println(buf.toList)
buf.clear()
println(buf)
val lb = ListBuffer(1, 2, 3)
lb += 4
lb.prepend(0)
println(lb)
val result = lb.toList
println(result)
val b = List.newBuilder[Int]
b += 1
b += 2
println(b.result())
val ab = ArrayBuffer(1,2,3,4,5,6)
ab.filterInPlace(_ % 2 == 0)
println(ab)
ab.mapInPlace(_ * 10)
println(ab)
ab.dropInPlace(1)
println(ab)
ArrayBuffer(a, b, c, d, e)
ArrayBuffer(z, a, m, b, c, d, e)
ArrayBuffer(m, b, c, d, e)
ArrayBuffer(Q, b, c, d, e)
List(Q, b, c, d, e)
ArrayBuffer()
ListBuffer(0, 1, 2, 3, 4)
List(0, 1, 2, 3, 4)
List(1, 2)
ArrayBuffer(2, 4, 6)
ArrayBuffer(20, 40, 60)
ArrayBuffer(40, 60)
ArrayBuffer keeps the elements in an array that doubles when full, so append is amortised constant time and indexing is
constant time, while prepend, insert and remove in the middle shift elements and are linear. ListBuffer keeps a linked
list with a pointer to its last cell, so prepend and append are constant time and toList is constant time too (it hands over
the list and marks the buffer as shared); indexing is linear. The Builder returned by List.newBuilder[Int] is the generic
form of the same pattern: += elements, then result().
A buffer is mutable, so a val buf = ArrayBuffer(...) still lets you change buf: val fixes the reference, not the
contents. Immutable collections with val cannot be changed at all, which is why they need no -=:
//> using scala 3.10.0
import scala.collection.mutable
@main def bad(): Unit =
val xs: Seq[Int] = List(1,2,3)
val buf: mutable.Buffer[Int] = xs
val l = List(1, 2)
l += 3
-- [E007] Type Mismatch Error: immbad.scala:5:33
5 | val buf: mutable.Buffer[Int] = xs
| ^^
| Found: (xs : Seq[Int])
| Required: scala.collection.mutable.Buffer[Int]
|
| longer explanation available when compiling with `-explain`
-- [E008] Not Found Error: immbad.scala:7:4
7 | l += 3
| ^^^^
|value += is not a member of List[Int] - did you mean l.!=? or perhaps l.++ or l.+: or l.==?
2 errors found
Compilation failed
(The compiler lists look-alike members because there is no += on List; the intended fix is a mutable collection, a
var with xs :+= x, or building a new list.) Seq is also not assignable to mutable.Buffer, the first error above, for the reason explained in
Pitfalls.
Source: Collections 2.13: List buffers
Array, ArraySeq and IArray
Array[T] is the JVM array. It is mutable, has a fixed size, is accessed and updated with parentheses (a(0) = 10, which
the compiler turns into a.update(0, 10)), and unlike every other collection it is reified: Array[Int] is a real int[]
and Array[String] is a real String[]. It is invariant in T (see Variance),
where Java arrays are covariant. It is not a Seq; the operations (map, sorted, toList, …) come from an implicit
conversion to an array wrapper, so they work on arrays too and map returns an Array.
//> using scala 3.10.0
import scala.collection.immutable.ArraySeq
@main def arrays(): Unit =
val a = Array(1, 2, 3)
a(0) = 10
println(a.mkString(","))
println(a.length)
println(a.toList)
val b = Array.ofDim[Int](2, 3)
b(1)(2) = 7
println(b.map(_.mkString("[", " ", "]")).mkString(" "))
println(a.map(_ * 2).mkString(","))
println(a.sorted.mkString(","))
println(Array(1, 2) == Array(1, 2))
println(Array(1, 2).sameElements(Array(1, 2)))
println(Array(1, 2).toSeq == Array(1, 2).toSeq)
println(a.getClass.getSimpleName)
val strs = Array("a", "b")
println(strs.getClass.getSimpleName)
val s: ArraySeq[Int] = ArraySeq(1, 2, 3)
println(s)
println(s.getClass.getName)
val w = ArraySeq.unsafeWrapArray(a)
a(1) = 99
println(w)
println(a.length)
println(a.sum)
10,2,3
3
List(10, 2, 3)
[0 0 0] [0 0 7]
20,4,6
2,3,10
false
true
true
int[]
String[]
ArraySeq(1, 2, 3)
scala.collection.immutable.ArraySeq$ofInt
ArraySeq(10, 99, 3)
3
112
Source: Scala 3 Book: Array and Collections: Immutable ArraySeqs
Points to note in the output: printing an Array prints an identity string, so go through mkString or toList;
Array(1, 2) == Array(1, 2) is false because arrays use reference equality, while sameElements and a conversion to Seq
compare contents; Array.ofDim[Int](2, 3) creates a two-dimensional array; and ArraySeq.unsafeWrapArray(a) shares storage
with a, which the printed ArraySeq(10, 99, 3) shows after a(1) = 99. For performance with primitives, Array[Int] avoids
boxing completely.
Because an array’s element type is needed at run time to create it, generic code that creates arrays needs a ClassTag:
//> using scala 3.10.0
def firstOf[T](xs: Array[T]): T = xs(0)
def make[T](x: T): Array[T] = Array(x)
@main def generic(): Unit =
println(firstOf(Array("x")))
println(make(1))
-- [E172] Type Error: classtag.scala:3:38
3 |def make[T](x: T): Array[T] = Array(x)
| ^
|No reflect.ClassTag available for T. You can fix this by changing [T] to [T: reflect.ClassTag].
1 error found
Compilation failed
Source: Collections: Arrays
The fix is a context bound, def make[T: reflect.ClassTag](x: T): Array[T], as the 3.10 message says (3.9.0 and 3.3.8 only
print "No ClassTag available for T"). Generic code that is
happy with any sequence should use Vector, List or ArraySeq.
IArray[T] (Scala 3) is an immutable array: an opaque type whose only difference from Array is the missing update
method. At run time it is a plain JVM array with no wrapper, which makes it the cheapest way to hold a read-only table of
primitives.
//> using scala 3.10.0
@main def ia(): Unit =
val a = IArray(3, 1, 2)
val b = a.map(_ + 1)
println(b.mkString(","))
println(a.sorted.mkString(","))
println(a(0))
println(a.length)
println(a.toList)
val c = a ++ IArray(9)
println(c.mkString(","))
println(a.updated(0, 7).mkString(","))
4,2,3
1,2,3
3
3
List(3, 1, 2)
3,1,2,9
7,1,2
Source: Scala API: IArray
updated, ++ and map return fresh IArray values, as for any collection; map on a primitive array needs a ClassTag for the
result, which is found automatically for Int. Mutation through the type is rejected:
//> using scala 3.10.0
@main def bad(): Unit =
val a = IArray(3, 1, 2)
a(0) = 5
-- [E008] Not Found Error: iarraybad.scala:4:2
4 | a(0) = 5
| ^
| value update is not a member of IArray[Int]
1 error found
Compilation failed
Range
A Range is an immutable sequence of integers described by a start, an end and a step. It is an IndexedSeq that never
stores the numbers: (1 to 1000000000).size is computed instantly. to includes the end, until excludes it, by sets the
step (negative to count down), and the same syntax works for Char and, through BigDecimal, for decimals.
//> using scala 3.10.0
package trailhead
@main def ranges(): Unit =
val r = 1 to 5
println(r)
println(r.toList)
println(1 until 5)
println(10 to 1 by -3)
println((1 to 3).map(_ * 2))
println((1 to 1000000000).size)
println(r.getClass.getName)
println(BigDecimal(1.0) to BigDecimal(2.0) by BigDecimal(0.5))
println('a' to 'e')
println((1 to 0).isEmpty)
Range 1 to 5
List(1, 2, 3, 4, 5)
Range 1 until 5
Range 10 to 1 by -3
Vector(2, 4, 6)
1000000000
scala.collection.immutable.Range$Inclusive
NumericRange 1.0 to 2.0 by 0.5
NumericRange a to e
true
Ranges are the standard way to loop (for i <- 0 until n, see Conditionals & Loops)
and are used in a for comprehension (For Comprehensions). A
Floating point has no Range: 1.0 to 2.0 by 0.5 is rejected with "value to is not a member of Double", so write the bounds
as BigDecimal (which gives a NumericRange, as in the output above) or use a plain loop. map on a Range returns a Vector, as the fifth output line shows.
Source: Scala 3 Book: Range and Collections 2.13: Ranges
Queue, ArrayDeque and StringBuilder
Scala has an immutable Queue (two lists, so enqueue and dequeue are amortised constant time) and a mutable
scala.collection.mutable.Queue, which since 2.13 is a thin subclass of ArrayDeque. ArrayDeque is the mutable
double-ended queue implemented as a circular buffer; it is also the right choice for a stack (append and removeLast).
StringBuilder builds a String in place and is the mutable Seq[Char]; it is the only mutable collection name in scope
without an import.
//> using scala 3.10.0
package trailhead
import scala.collection.immutable.Queue
import scala.collection.mutable
@main def queues(): Unit =
val q = Queue(1, 2)
val q2 = q.enqueue(3)
val (head, rest) = q2.dequeue
println(s"$head $rest")
println(q2.enqueueAll(List(4, 5)))
val dq = mutable.ArrayDeque(1, 2, 3)
dq.prepend(0)
dq.append(4)
println(dq)
println(dq.removeHead())
println(dq.removeLast())
println(dq)
println(dq(1))
val mq = mutable.Queue("a", "b")
mq.enqueue("c")
println(mq.dequeue())
println(mq)
val sb = StringBuilder()
sb.append("hike").append(' ').append(42)
sb ++= "!"
println(sb)
println(sb.length)
sb.insert(0, ">> ")
println(sb.toString)
println(sb.getClass.getName)
1 Queue(2, 3)
Queue(1, 2, 3, 4, 5)
ArrayDeque(0, 1, 2, 3, 4)
0
4
ArrayDeque(1, 2, 3)
2
a
Queue(b, c)
hike 42!
8
>> hike 42!
scala.collection.mutable.StringBuilder
The immutable Queue returns a new queue from enqueue and a pair (head, rest) from dequeue, so a state machine over it
is a fold. The mutable queue returns the element and changes itself. ArrayDeque supports prepend and append in
amortised constant time and removeHead and removeLast, plus constant time indexing. scala.collection.mutable.Stack
exists (it was deprecated in 2.12 and reimplemented in 2.13) but ArrayDeque is the usual choice.
What every Seq can do
Seq adds position-based operations to the Iterable vocabulary: apply, isDefinedAt, lift, indexOf, contains,
padTo, updated, patch, prepended/appended, sorted, reverse/reverseIterator, search (binary search in a sorted
sequence), startsWith, combinations, diff, intersect and concat. A few are worth seeing in action. Note the last
line: s(10) on a three element Seq throws.
//> using scala 3.10.0
package trailhead
@main def seqs(): Unit =
val s = Seq(1, 2, 3)
println(s.lengthCompare(2))
println(s.isDefinedAt(5))
println(s.lift(5))
println(s.lift(1))
println(s.indexOf(3))
println(s.contains(2))
println(s.padTo(5, 0))
println(s.updated(0, 7))
println(s.patch(1, List(9, 9), 1))
println(s.prepended(0))
println(s.appended(4))
println(s.sorted(using Ordering[Int].reverse))
println(s.reverseIterator.toList)
println(s.search(2))
println(s.startsWith(Seq(1, 2)))
println(s.combinations(2).toList)
println(s.diff(Seq(1)))
println(s.intersect(Seq(2, 3, 4)))
println(s.concat(Seq(4)))
println(s(10))
1
false
None
Some(2)
2
true
List(1, 2, 3, 0, 0)
List(7, 2, 3)
List(1, 9, 9, 3)
List(0, 1, 2, 3)
List(1, 2, 3, 4)
List(3, 2, 1)
List(3, 2, 1)
Found(1)
true
List(List(1, 2), List(1, 3), List(2, 3))
List(2, 3)
List(2, 3)
List(1, 2, 3, 4)
Exception in thread "main" java.lang.IndexOutOfBoundsException: 10
at scala.collection.LinearSeqOps.apply(LinearSeq.scala:141)
at scala.collection.LinearSeqOps.apply$(LinearSeq.scala:45)
at scala.collection.immutable.List.apply(List.scala:83)
lift is the safe version of apply: it returns an Option instead of throwing, which is the idiomatic way to read a
position that may not exist. search returns Found(index) or InsertionPoint(index). union is deprecated since 2.13.0
in favour of concat ("Use concat instead"). The exception from s(10) on a List is an IndexOutOfBoundsException; the
stack frames show that List reaches the element through LinearSeqOps.apply, walking the list.
Indexed versus linear
Seq has two refinements that declare the performance contract. IndexedSeq promises fast random access and length
(Vector, Range, ArrayBuffer, ArrayDeque, ArraySeq, String through an implicit wrapper), and LinearSeq promises fast
head, tail and isEmpty (List, LazyList, Queue). If a method indexes into its argument, ask for an IndexedSeq; if it
only walks from the front, ask for Seq. A plain Seq gives no promise at all, and passing a List to code that loops with
xs(i) is a classic quadratic trap.
Pattern matching on sequences
Every Seq can be matched with the Seq(...) extractor pattern, including a trailing wildcard that captures the rest, and the
extractors +: and :+ match the first and last element (on Vector and other sequences; :: is List only). The
extractor machinery is described on Extractors & Pattern Bindings.
//> using scala 3.10.0
package trailhead
import scala.collection.mutable
def describe(xs: Seq[String]): String = xs match
case Seq() => "no stops"
case Seq(only) => s"one stop: $only"
case Seq(first, second) => s"two stops: $first, $second"
case Seq(first, rest*) => s"$first and ${rest.size} more"
def lastOf[A](xs: Vector[A]): Option[A] = xs match
case init :+ last => Some(last)
case _ => None
@main def patterns(): Unit =
println(describe(Nil))
println(describe(List("Hut")))
println(describe(Vector("Hut", "Ridge")))
println(describe(List("Hut", "Ridge", "Pass", "Summit")))
println(lastOf(Vector(1, 2, 3)))
println(lastOf(Vector.empty[Int]))
val stack = mutable.ArrayDeque[Int]()
stack.append(1)
stack.append(2)
println(stack.removeLast())
no stops
one stop: Hut
two stops: Hut, Ridge
Hut and 3 more
Some(3)
None
2
In case Seq(first, rest*), rest* binds the remaining elements as a Seq. In Scala 2 the same pattern was written
rest @ _*; the Scala 3 spelling is rest*, and the old spelling still compiled without any warning on 3.10.0 (checked
with and without -source:future). The pattern init : last+ is cheap on a Vector; on a List it would walk the entire list.
Comparison with Java and Kotlin
| Aspect | Scala | Java | Kotlin |
|---|---|---|---|
Default list |
|
|
|
Growable sequence |
|
|
|
Immutable general purpose |
|
|
Read-only |
Append to an immutable list |
|
Not available; copy. |
|
Arrays |
|
|
|
Ranges |
|
|
|
Safe indexing |
|
|
|
Where Java code uses Optional the Scala code uses Option, which is why lift is natural; see
Optional and Option & Null Safety
for that side, and Kotlin Null Safety for the Kotlin approach with ?.
Pitfalls
-
SeqisListby default.Seq(1, 2, 3)builds aList, so indexed access in a loop is quadratic. WriteVector(...)when you index. -
+=on an immutable collection. The compiler error isvalue += is not a member of List[Int]; there is no in-place growth on an immutable collection. -
Mutable collections do not conform to
Seq. Passing anArrayBufferwhereSeqis expected fails with E007 (Found: ArrayBuffer[Int],Required: Seq[Int]). Acceptscala.collection.Seqor convert withtoSeq. -
Arrays compare by reference and print as identity strings; wrap or convert them before comparing or printing.
-
Generic arrays need a
ClassTag(error E172 shown above). -
ArraySeq.unsafeWrapArrayaliases the array. Never mutate the array afterwards unless you want theArraySeqto change too. -
Slow loops over
List.xs(i),xs.length,xs.lastand:+are linear. -
Out-of-range index throws
IndexOutOfBoundsException; uselift,isDefinedAtorapplyOrElse. -
Appending in a
ListBufferafter callingtoList.toListis constant time, but later modifications of the buffer copy it first; prefer to finish building before converting.
Where the books are outdated
-
Material for Scala 2.12 and earlier describes
scala.Seqasscala.collection.Seq(so mutable collections conform) and showsStreamas the lazy list; since 2.13Seqis immutable andStreamis deprecated in favour ofLazyList. -
Material from before 2.13 describes
WrappedArray; in 2.13 and 3 the wrappers areArraySeq.ofIntand friends, as theArraySeq$ofIntclass name in the output shows. -
Seq.unionis deprecated since 2.13.0; useconcat. -
IArrayand opaque types do not exist in Scala 2; a Scala 3 book chapter on them (or the reference on opaque types) is the only source of theupdate-less array.
Scala 3.9 LTS versus 3.10
The sequence classes and operations behave identically on 3.3.8, 3.9.0 and 3.10.0: every program on this page was compiled
and run on all three and produced the same output. The one visible difference is the compiler message for a missing
ClassTag when creating a generic array, error E172: 3.10.0 says "No reflect.ClassTag available for T. You can fix this by
changing [T] to [T: reflect.ClassTag]." while 3.9.0 and 3.3.8 only say "No ClassTag available for T". The other error messages
on this page (E007, E008 for IArray, and E008 for +=) are identical on the three versions.