Collections and functional style

The collection hierarchy, the transformations you use daily, and for-comprehensions for readable pipelines.

The collection hierarchy

Scala separates an immutable hierarchy from a mutable one, and the immutable types are imported by default. The names are the same, so the difference you notice in code is whether a method returns a new collection or updates in place.

CollectionOrdered?Notes
ListYesImmutable linked list: O(1) prepend, O(n) index access
VectorYesEffectively O(1) index and update; the default for large sequences
ArraySeqYesAn immutable wrapper over an array
SetNoHashSet for membership, SortedSet to keep order
MapNoKey and value lookup; SortedMap or ListMap keep order
RangeYesA sequence with no storage, such as 1 to 1000
LazyListYesComputed on demand, so it can be infinite
val xs = List(1, 2, 3, 4)
val ys = Vector(1, 2, 3)

val prepended = 0 +: xs         // O(1) for a List
val appended  = xs :+ 5         // O(n) for a List
val joined    = xs ++ ys

xs.head                         // 1
xs.tail                         // List(2, 3, 4)
xs.take(2)                      // List(1, 2)
xs.drop(2)                      // List(3, 4)
xs.sliding(2).toList            // List(List(1,2), List(2,3), List(3,4))
xs.grouped(3).toList            // List(List(1,2,3), List(4))
xs.lift(10)                     // None, instead of an exception

Transformations

val numbers = (1 to 10).toList

numbers.map(_ * 2)                              // transform every element
numbers.filter(_ % 2 == 0)                      // keep some
numbers.collect { case n if n > 5 => n * n }    // transform and filter in one pass
numbers.foldLeft(0)(_ + _)                      // combine, starting from a value
numbers.reduce(_ + _)                           // combine, and fail on an empty collection
numbers.partition(_ % 2 == 0)                   // a pair: evens and odds
numbers.groupBy(_ % 3)                          // a Map from key to bucket
numbers.sortBy(-_)                              // descending
numbers.zipWithIndex.map { case (n, i) => s"$i:$n" }

val words = List("delta", "alpha", "beta")
words.min                                       // "alpha", using Ordering[String]
words.maxBy(_.length)
words.mkString("[", ", ", "]")                  // "[delta, alpha, beta]"

// a pipeline, read top to bottom: each step produces a new collection
val result = (1 to 100).toList
  .filter(_ % 3 == 0)
  .map(n => n * n)
  .take(5)
  .sum
  • map, filter, flatMap and fold are the core four; most other methods are conveniences built from them.
  • Use collect when you want to transform some elements and discard the rest in a single pass.
  • view makes a chain lazy, so a long sequence is not materialised at every step — better for performance and often clearer.
  • foldLeft takes an initial value, so it works on an empty collection; reduce does not and will throw.

for-comprehensions

A for comprehension is sugar over map, flatMap and withFilter. That is why it works for Option, Either and Future as well as for collections.

val people = List(("Ada", 36), ("Grace", 45), ("Alan", 41))

val adults = for {
  (name, age) <- people
  if age >= 40
} yield name                                   // List(Grace, Alan)

// the same thing written with explicit calls
val adults2 = people.withFilter { case (_, age) => age >= 40 }.map(_._1)

// Option composes without nested matches
def lookup(id: Long): Option[String] = ???
def lookupAge(name: String): Option[Int] = ???

val summary: Option[String] = for {
  name <- lookup(1)
  age  <- lookupAge(name)
} yield s"$name is $age"

// Future composes exactly the same way
val report = for {
  user  <- fetchUser(1)
  items <- fetchItems(user.id)
} yield Report(user, items)
💡
One syntax, one set of rules, several behaviours: a comprehension over a List produces every combination, over an Option it short-circuits at the first None, and over a Future it sequences asynchronous work. Learn the desugaring once and you can read all of them.

FAQ

List or Vector?
List when you build by prepending and walk front to back; Vector when you index or update large sequences. Below a few thousand elements the difference is rarely worth measuring.
Why is my chain of operations slow?
Each step on a List allocates an intermediate collection. Insert .view before the chain to fuse the steps, switch to Vector, or use iterator when a single pass is enough.

Case classes and pattern matching Syntax and immutability

Last refreshed 2026-09-18.