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)

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)

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)

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))

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)

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))

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) computes f for every element and allocates two lists. Add .view (or use an Iterator) to fuse the pipeline and stop early; see Views, Iterators & LazyList.

  • reduce on an empty collection throws. Use reduceOption or fold.

  • The fold accumulator type comes from the initial value. foldLeft(0) cannot accumulate a Double.

  • groupBy returns an unordered Map. Do not rely on the key order of the printed result.

  • take, drop and slice never fail, but indexing does. xs(10) throws on a short list, whereas xs.lift(10) returns an Option.

  • sliding and grouped return iterators, so the first element of the result is computed when you ask for it and the iterator can be traversed only once.

  • sortBy needs an Ordering for the key type. A key of your own class needs a given Ordering.

  • Side effects inside map and filter run strictly on lists and lazily on views, so the same code prints in different orders; use tapEach or foreach deliberately.

Where the books are outdated

  • Scala 2 books write case (a, b) => to destructure tuples in map, filter and foldLeft; Scala 3 parameter untupling lets you write (a, b) =>.

  • Books written before 2.13 use mapValues on a Map and rely on it returning a lazy view that recomputes on every access; in 2.13 it is deprecated on Map and replaced by .view.mapValues(...), optionally followed by toMap.

  • groupMap and groupMapReduce (2.13) are missing from older material, which uses groupBy followed by mapValues.

  • Stream and the .par parallel collections are no longer part of the standard library; see Views, Iterators & LazyList and Parallel Collections & Concurrency Libraries.

  • Material that calls .force on a view should use toList, to(Vector) or another explicit conversion; View.force is deprecated since 2.13.0.