Collection Operations
|
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). The collections library is the Scala 2.13 library that all Scala 3 versions share, so the operations below behave the same on the 3.9 LTS and 3.3 LTS lines and on Scala 2.13.
The methods on this page are defined once, in the shared collection traits, and are available on every sequence, set and map. They are what a Scala programmer writes in place of loops. For the collection types themselves see Collections Overview, Sequences and Sets & Maps; for the lazy variants of these operations see Views, Iterators & LazyList.
The examples use the trailhead scenario of this section: a toolkit that plans hikes on a network of mountain trails. Most of them run against this list:
case class Trail(name: String, km: Double, climb: Int, region: String)
val trails = List(
Trail("Aspen Loop", 8.5, 420, "North"),
Trail("Birch Ridge", 12.0, 950, "North"),
Trail("Cedar Creek", 5.2, 120, "South"),
Trail("Dune Walk", 3.0, 40, "South"),
Trail("Eagle Pass", 16.4, 1500, "East"))
Strict, immutable and returning a new collection
Every operation here is strict on the default collections: trails.map(f) runs f for every element immediately and
returns a new collection. The immutable collections List, Vector, Set and Map are never changed by an operation;
the mutable ones (ArrayBuffer, mutable.Map) have the same transformation methods, which also return new collections,
and separate mutating methods such as +=. The result has the same collection type as the source where it can
(List in, List out). This is the design described in
Collections Architecture & Custom Collections.
If you know Java, the operations correspond to Stream pipelines (see
Streams & Collectors) with three differences: there is no
stream() and no collect(Collectors.toList()) step, the result is a collection straight away, and nothing is lazy
unless you ask for it with view. Kotlin’s collection functions (see
Collections & Sequences) are the closest relatives, with
the same strict behavior and the same laziness opt-in (asSequence in Kotlin, view in Scala).
Transforming: map, flatMap, flatten and collect
map applies a function to each element and keeps the shape. flatMap applies a function that returns a collection
(or an Option, or any IterableOnce) and concatenates the results.
//> using scala 3.10.0
@main def b1(): Unit =
val names = trails.map(_.name)
println(names)
println(trails.map(t => (t.name, t.km)).take(2))
println(trails.flatMap(t => List(t.name, t.region)).take(4))
println(trails.flatMap(t => Option.when(t.climb > 900)(t.name)))
println(List(List(1, 2), Nil, List(3)).flatten)
println(List("12", "x", "7").flatMap(_.toIntOption))
List(Aspen Loop, Birch Ridge, Cedar Creek, Dune Walk, Eagle Pass)
List((Aspen Loop,8.5), (Birch Ridge,12.0))
List(Aspen Loop, North, Birch Ridge, North)
List(Birch Ridge, Eagle Pass)
List(1, 2, 3)
List(12, 7)
Because flatMap accepts an Option, flatMap(f) where f returns an Option is the idiom for "map and drop the
failures": List("12", "x", "7").flatMap(_.toIntOption) keeps only the strings that parse. flatMap is also what a for
comprehension with several generators becomes; see For Comprehensions.
collect combines filter and map in one step. It takes a partial function (see
Partial Functions) and keeps the elements for which that function is
defined, replaced by its result. It is the natural way to filter by type or by pattern:
//> using scala 3.10.0
@main def b1b(): Unit =
println(List(1, "two", 3.0, 4).collect { case i: Int => i * 10 })
println(List("x", "12", "7").collectFirst { case s if s.forall(_.isDigit) => s.toInt })
List(10, 40)
Some(12)
collectFirst stops at the first match and returns an Option. Both are shown with the pattern forms of
Pattern Matching. In Scala 3.10 the SIP-74 preview syntax
(-preview) allows xs.collect(case (a, b) if b > 2 => a) without the braces; it is a preview feature and is not used here.
tapEach runs a side effect for each element and passes the elements through unchanged. It is useful to log in the middle
of a chain; on a strict collection it runs at once, on a view or iterator it runs only when the elements are pulled.
//> using scala 3.10.0
@main def b1c(): Unit =
val seen = List(1, 2, 3).tapEach(x => println(s"saw $x")).map(_ * 2)
println(seen)
saw 1
saw 2
saw 3
List(2, 4, 6)
Source: Scala API: IterableOps (tapEach)
Filtering, searching and slicing
filter keeps the elements that satisfy a predicate, filterNot the others. find returns the first match as an
Option; exists, forall and count answer questions without building a collection.
//> using scala 3.10.0
@main def b1d(): Unit =
println(trails.filter(_.km > 8).map(_.name))
println(trails.filterNot(_.km > 8).map(_.name))
println(trails.find(_.region == "South"))
println(trails.exists(_.climb > 1000))
println(trails.forall(_.km > 3))
println(trails.count(_.region == "North"))
List(Aspen Loop, Birch Ridge, Eagle Pass)
List(Cedar Creek, Dune Walk)
Some(Trail(Cedar Creek,5.2,120,South))
true
false
2
partition splits a collection in two with one predicate, and span splits at the first element that fails it. Both
return a tuple, which is usually destructured immediately (see
Tuples & Named Tuples):
//> using scala 3.10.0
@main def b1e(): Unit =
val (easy, hard) = trails.partition(_.km < 10)
println(easy.map(_.name))
println(hard.map(_.name))
val (leading, rest) = trails.span(_.km > 5)
println(leading.map(_.name))
println(rest.map(_.name))
List(Aspen Loop, Cedar Creek, Dune Walk)
List(Birch Ridge, Eagle Pass)
List(Aspen Loop, Birch Ridge, Cedar Creek)
List(Dune Walk, Eagle Pass)
Source: Collections: Trait Iterable
span stopped at Dune Walk (3.0 km), so Eagle Pass is in the second half even though it is long; partition looks at
every element, span stops at the first failure.
The slicing methods never throw on out-of-range arguments, unlike indexing:
//> using scala 3.10.0
@main def b1f(): Unit =
val names = trails.map(_.name)
println(names.take(2))
println(names.drop(3))
println(names.slice(1, 3))
println(names.takeRight(1))
println(names.dropRight(4))
println(names.takeWhile(_ != "Cedar Creek"))
println(names.dropWhile(_ != "Cedar Creek"))
println(names.take(99).size)
List(Aspen Loop, Birch Ridge)
List(Dune Walk, Eagle Pass)
List(Birch Ridge, Cedar Creek)
List(Eagle Pass)
List(Aspen Loop)
List(Aspen Loop, Birch Ridge)
List(Cedar Creek, Dune Walk, Eagle Pass)
5
take(99) on a five-element list returns all five elements instead of failing. slice(from, until) is half-open, like
substring.
Folding and reducing
A fold collapses a collection into one value, starting from an initial value; a reduce does the same without an initial value and therefore needs at least one element.
//> using scala 3.10.0
@main def b2(): Unit =
println(trails.map(_.climb).foldLeft(0)(_ + _))
println(trails.foldLeft("")((acc, t) => acc + t.name.head))
println(List("a", "b", "c").foldLeft("")((acc, x) => s"($acc$x)"))
println(List("a", "b", "c").foldRight("")((x, acc) => s"($x$acc)"))
println(List(1, 2, 3).foldLeft(Vector.empty[Int])((acc, x) => x +: acc))
3030
ABCDE
(((a)b)c)
(a(b(c)))
Vector(3, 2, 1)
The last line shows that the accumulator may have a different type from the elements, here Vector[Int] while the
elements are Int; that is what makes foldLeft the general-purpose building block.
foldLeft processes the elements from the left, with the accumulator as the first argument of the function; foldRight
processes them from the right, with the accumulator as the second argument. The nested parentheses make the association
visible. Prefer foldLeft: it is the natural fit for an accumulator that grows as you go. foldRight is needed when the
result is built from the right, such as consing onto a list. It is safe on large collections: the library implements it by
reversing, not by recursion (a foldRight over 200000 elements ran without a stack overflow on JDK 26).
The type of the accumulator is fixed by the initial value, which is the usual source of type errors:
//> using scala 3.10.0
case class Trail(name: String, km: Double)
@main def b2err2(): Unit =
val trails = List(Trail("A", 2.0))
println(trails.foldLeft(0)(_ + _.km))
-- [E007] Type Mismatch Error: b2err2.scala:5:29
5 | println(trails.foldLeft(0)(_ + _.km))
| ^^^^^^^^
| Found: Double
| Required: Int
The accumulator is an Int because of the 0; start from 0.0 to accumulate a Double.
reduce and its variants take the first element as the starting value. On an empty collection reduce throws, so use
reduceOption when emptiness is possible, or a fold with a neutral element:
//> using scala 3.10.0
@main def b2b(): Unit =
println(trails.map(_.climb).reduce(_ max _))
println(trails.map(_.climb).reduceOption(_ max _))
println(List.empty[Int].reduceOption(_ max _))
println(List.empty[Int].fold(0)(_ + _))
println(List.empty[Int].sum)
println(List(1, 2, 3, 4).scanLeft(0)(_ + _))
println(trails.maxBy(_.climb).name)
println(trails.minByOption(_.km).map(_.name))
1500
Some(1500)
None
0
0
List(0, 1, 3, 6, 10)
Eagle Pass
Some(Dune Walk)
scanLeft is foldLeft that keeps every intermediate accumulator (a running total). sum, product, min, max,
maxBy and minBy are the common folds with names; the Option variants (maxByOption and the like) of the last four return None on an empty
collection. Calling reduce on an empty list fails at run time:
//> using scala 3.10.0
@main def b2err(): Unit =
List.empty[Int].reduce(_ max _)
Exception in thread "main" java.lang.UnsupportedOperationException: empty.reduceLeft
at scala.collection.IterableOnceOps.reduceLeft$(IterableOnce.scala:879)
...
reduceLeft and reduceRight fix the direction in the same way as the folds, which matters for operators that are not
associative. Over List(1, 2, 3, 4, 5, 6), reduceLeft(_ - _) gives -19, reduceRight(_ - _) gives -3, and
foldLeft(0)(_ - _) gives -21. reduce leaves the order unspecified (so it may be parallelised), and should be used only
with an associative operator.
A fold can carry several accumulators at once by using a tuple, which computes a sum and a count in a single pass:
//> using scala 3.10.0
@main def b2c(): Unit =
println(List(10, 20, 30).foldLeft((0, 0))((acc, x) => (acc._1 + x, acc._2 + 1)))
(60,3)
Grouping: groupBy, groupMap and groupMapReduce
groupBy partitions the collection into a Map from a key to the elements with that key. groupMap also transforms the
elements of each group, and groupMapReduce also reduces each group to one value. The last two do in one pass what
groupBy(...).view.mapValues(...) does in two, without building the intermediate groups of whole elements.
//> using scala 3.10.0
@main def b2d(): Unit =
println(trails.groupBy(_.region).view.mapValues(_.map(_.name)).toMap)
println(trails.groupMap(_.region)(_.name))
println(trails.groupMapReduce(_.region)(_.km)(_ + _))
println(trails.map(_.region).groupBy(identity).view.mapValues(_.size).toMap)
Map(South -> List(Cedar Creek, Dune Walk), East -> List(Eagle Pass), North -> List(Aspen Loop, Birch Ridge))
Map(South -> List(Cedar Creek, Dune Walk), North -> List(Aspen Loop, Birch Ridge), East -> List(Eagle Pass))
Map(South -> 8.2, North -> 20.5, East -> 16.4)
Map(South -> 2, East -> 1, North -> 2)
Source: Collections: Trait Iterable
The three-argument shape of groupMapReduce(key)(value)(combine) reads as "group by region, take the length of each trail,
add them up": total kilometres per region. The key order of the printed map is not meaningful: the key order of a groupBy or
groupMap result is unspecified, and the maps above print the same three keys in different orders. Sort the result
(.toList.sortBy(_._1)) when you need a stable order. The groupBy method on a List returns an immutable Map whose
values keep the encounter order of the original elements.
partitionMap splits by a function that returns Either, producing two collections of possibly different element types:
//> using scala 3.10.0
@main def b2e(): Unit =
println(List(1, 2, 3, 4).partitionMap(i => if i % 2 == 0 then Right(i) else Left(s"odd$i")))
(List(odd1, odd3),List(2, 4))
Source: Collections: Trait Iterable
Either is covered in Either & Try.
Combining: zip and friends
zip pairs elements by position and stops at the shorter collection. zipWithIndex pairs each element with its position,
unzip is the inverse, zipAll pads the shorter side, and lazyZip avoids building the intermediate tuples:
//> using scala 3.10.0
@main def b3(): Unit =
val names = List("Aspen Loop", "Birch Ridge", "Cedar Creek", "Dune Walk", "Eagle Pass")
val kms = List(8.5, 12.0, 5.2, 3.0, 16.4)
println(names.zip(kms))
println(names.zip(List(1, 2)))
println(names.zipWithIndex.take(2))
println(names.zip(kms).unzip)
println(names.zipAll(List(1, 2), "?", 0).takeRight(2))
println(List(1, 2, 3).lazyZip(List(4, 5, 6)).map(_ * _))
List((Aspen Loop,8.5), (Birch Ridge,12.0), (Cedar Creek,5.2), (Dune Walk,3.0), (Eagle Pass,16.4))
List((Aspen Loop,1), (Birch Ridge,2))
List((Aspen Loop,0), (Birch Ridge,1))
(List(Aspen Loop, Birch Ridge, Cedar Creek, Dune Walk, Eagle Pass),List(8.5, 12.0, 5.2, 3.0, 16.4))
List((Dune Walk,0), (Eagle Pass,0))
List(4, 10, 18)
Source: Collections: Trait Iterable
Zipping a collection with its own tail gives consecutive pairs, a common way to compute differences:
xs.zip(xs.tail).map((a, b) => b - a) gives List(1, 1, 1, 1, 1) for List(1, 2, 3, 4, 5, 6). The parameter
untupling in (a, b) => ... is a Scala 3 feature (in Scala 2 it needed case (a, b) =>). ++ concatenates, +: and :+
prepend and append, and diff, intersect and union treat the collections as multisets.
Windows: sliding and grouped
grouped(n) cuts a collection into consecutive blocks of up to n elements. sliding(n) returns overlapping windows, and
sliding(size, step) lets you control the overlap. Both return an Iterator, so call toList to see the result.
//> using scala 3.10.0
@main def b3b(): Unit =
println(List(1, 2, 3, 4, 5).sliding(3).toList)
println(List(1, 2, 3, 4, 5).sliding(2, 2).toList)
println(List(1, 2, 3, 4, 5).grouped(2).toList)
println(List(1, 2, 3, 4, 5).sliding(3).map(_.sum / 3.0).toList)
println(List(1, 2).sliding(5).toList)
List(List(1, 2, 3), List(2, 3, 4), List(3, 4, 5))
List(List(1, 2), List(3, 4), List(5))
List(List(1, 2), List(3, 4), List(5))
List(2.0, 3.0, 4.0)
List(List(1, 2))
Source: Collections: Trait Iterable
The fourth line is a moving average of window 3. The last line shows that a collection shorter than the window yields one
short window rather than none. On an Iterator (not a List) the blocks are ArraySeq`s, as in
`Iterator(1, 2, 3, 4, 5, 6, 7).grouped(3), which produces ArraySeq(1, 2, 3), ArraySeq(4, 5, 6), ArraySeq(7).
Sorting and removing duplicates
sorted uses the implicit Ordering of the element type. sortBy sorts by a key, whose type needs an Ordering
(tuples have one, which gives multi-key sorts). sortWith takes an explicit "less than". All three are stable and return a
new collection; reverse reverses it.
//> using scala 3.10.0
@main def b3c(): Unit =
val names = List("Aspen Loop", "Birch Ridge", "Cedar Creek", "Dune Walk", "Eagle Pass")
println(List(3, 1, 2).sorted)
println(names.sortBy(_.length))
println(names.sortBy(n => (n.length, n)).reverse)
println(names.sortBy(_.length)(using Ordering[Int].reverse))
println(names.sortWith(_.last < _.last))
println(names.distinctBy(_.length))
println(List(1, 2, 2, 3, 1).distinct)
List(1, 2, 3)
List(Dune Walk, Aspen Loop, Eagle Pass, Birch Ridge, Cedar Creek)
List(Cedar Creek, Birch Ridge, Eagle Pass, Aspen Loop, Dune Walk)
List(Birch Ridge, Cedar Creek, Aspen Loop, Eagle Pass, Dune Walk)
List(Birch Ridge, Cedar Creek, Dune Walk, Aspen Loop, Eagle Pass)
List(Aspen Loop, Birch Ridge, Dune Walk)
List(1, 2, 3)
Aspen Loop and Eagle Pass both have ten characters, and the stable sort keeps them in their original order. The third
line sorts by (length, name) and then reverses. The fourth passes an Ordering explicitly with using (see
Givens & Using Clauses), which reverses the key order
without reversing the tie-breaking. distinctBy keeps the first element for each key.
Chaining ordinary values: pipe and tap
scala.util.chaining adds two methods to every value. pipe passes the value to a function and returns the result, so a
chain of plain functions reads left to right. tap runs a side effect and returns the original value, which is the
single-value counterpart of tapEach.
//> using scala 3.10.0
import scala.util.chaining.*
@main def b3d(): Unit =
println(5.pipe(_ + 1).pipe(_ * 2))
println(List(3, 1, 2).tap(l => println(s"before sort: $l")).sorted)
12
before sort: List(3, 1, 2)
List(1, 2, 3)
The result type of map on Map and Set
The element type of the result decides the collection type. Set.map can shrink the set because the results are put into
a new Set. Map.map gives a Map only when the function returns a pair; otherwise it falls back to a general Iterable.
//> using scala 3.10.0
@main def b3e(): Unit =
println(Map("a" -> 1, "b" -> 2).map((k, v) => (k.toUpperCase, v * 2)))
println(Map("a" -> 1, "b" -> 2).map((k, v) => k))
println(Set(1, 2, 3).map(_ % 2))
println(List(1, 2, 3).map(_ % 2))
Map(A -> 2, B -> 4)
List(a, b)
Set(1, 0)
List(1, 0, 1)
The second line returns a List (the static type is Iterable[String]), the third has two elements where the input had
three. Use mapValues through a view, map on .values or .keys, or toList.map when you want to be explicit about the
intent.
Pitfalls
-
Chaining strict operations builds intermediate collections.
xs.map(f).filter(p).take(3)computesffor every element and allocates two lists. Add.view(or use anIterator) to fuse the pipeline and stop early; see Views, Iterators & LazyList. -
reduceon an empty collection throws. UsereduceOptionorfold. -
The fold accumulator type comes from the initial value.
foldLeft(0)cannot accumulate aDouble. -
groupByreturns an unorderedMap. Do not rely on the key order of the printed result. -
take,dropandslicenever fail, but indexing does.xs(10)throws on a short list, whereasxs.lift(10)returns anOption. -
slidingandgroupedreturn iterators, so the first element of the result is computed when you ask for it and the iterator can be traversed only once. -
sortByneeds anOrderingfor the key type. A key of your own class needs a givenOrdering. -
Side effects inside
mapandfilterrun strictly on lists and lazily on views, so the same code prints in different orders; usetapEachorforeachdeliberately.
Where the books are outdated
-
Scala 2 books write
case (a, b) =>to destructure tuples inmap,filterandfoldLeft; Scala 3 parameter untupling lets you write(a, b) =>. -
Books written before 2.13 use
mapValueson aMapand rely on it returning a lazy view that recomputes on every access; in 2.13 it is deprecated onMapand replaced by.view.mapValues(...), optionally followed bytoMap. -
groupMapandgroupMapReduce(2.13) are missing from older material, which usesgroupByfollowed bymapValues. -
Streamand the.parparallel collections are no longer part of the standard library; see Views, Iterators & LazyList and Parallel Collections & Concurrency Libraries. -
Material that calls
.forceon a view should usetoList,to(Vector)or another explicit conversion;View.forceis deprecated since 2.13.0.