Scala map, flatMap, filter, and fold: Functional Operations

TT

Scala map, flatMap, filter, and fold: Functional Operations

The functional collection operations are the bread and butter of Scala programming. Understanding map, flatMap, filter, and fold is essential — they replace most loops and enable clean, composable data transformations. This module covers each operation with practical examples.

map: Transform Every Element

map applies a function to every element and returns a new collection:

scala
val numbers = List(1, 2, 3, 4, 5)

val doubled = numbers.map(_ * 2)
println(doubled)  // List(2, 4, 6, 8, 10)

val strings = numbers.map(n => s"item-$n")
println(strings)  // List(item-1, item-2, item-3, item-4, item-5)

map preserves the structure — a List maps to a List, a Vector maps to a Vector.

filter: Keep Matching Elements

filter keeps only elements that satisfy a predicate:

scala
val evens = numbers.filter(_ % 2 == 0)
println(evens)  // List(2, 4)

val odds = numbers.filterNot(_ % 2 == 0)
println(odds)   // List(1, 3, 5)

flatMap: Map Then Flatten

flatMap applies a function that returns a collection for each element, then flattens the results:

scala
val words = List("Hello World", "Scala Programming", "Functional Style")

val allWords = words.flatMap(_.split(" "))
println(allWords)  // List(Hello, World, Scala, Programming, Functional, Style)

Compare map vs flatMap:

scala
words.map(_.split(" ").toList)
// List(List(Hello, World), List(Scala, Programming), List(Functional, Style))

words.flatMap(_.split(" ").toList)
// List(Hello, World, Scala, Programming, Functional, Style)

flatMap is also the key to chaining Option and Either — it lets you write flat code instead of nested match expressions.

foldLeft: Aggregate from Left to Right

foldLeft accumulates a result by applying a function to each element and the running accumulator:

scala
val sum = numbers.foldLeft(0)(_ + _)
println(sum)  // 15

val product = numbers.foldLeft(1)(_ * _)
println(product)  // 120

// Building a string from a list
val sentence = List("Scala", "is", "expressive").foldLeft("")(
  (acc, word) => if (acc.isEmpty) word else s"$acc $word"
)
println(sentence)  // Scala is expressive

The signature is foldLeft[B](initial: B)(f: (B, A) => B): B. The first argument is the starting value; the function takes the accumulator and current element.

foldRight: Aggregate from Right to Left

foldRight works the same way but processes elements from right to left. This matters for non-associative operations:

scala
val rightToLeft = numbers.foldRight(List.empty[Int])(_ :: _)
println(rightToLeft)  // List(1, 2, 3, 4, 5) — preserves order

// foldLeft with :: would reverse the list:
val reversed = numbers.foldLeft(List.empty[Int])((acc, x) => x :: acc)
println(reversed)  // List(5, 4, 3, 2, 1)

reduce: foldLeft Without an Initial Value

reduce folds without an initial value — the first element is used as the starting accumulator:

scala
val sum = numbers.reduce(_ + _)           // 15
val max = numbers.reduce((a, b) => if (a > b) a else b)  // 5

reduce throws UnsupportedOperationException on an empty collection. Use reduceOption for safety, or foldLeft with a neutral element.

groupBy: Partition into a Map

groupBy splits a collection into a Map based on a key function:

scala
case class Person(name: String, department: String)

val people = List(
  Person("Alice", "Engineering"),
  Person("Bob", "Marketing"),
  Person("Carol", "Engineering"),
  Person("Dave", "Marketing"),
  Person("Eve", "Engineering")
)

val byDepartment = people.groupBy(_.department)
println(byDepartment("Engineering").map(_.name))
// List(Alice, Carol, Eve)

partition: Split into Two Lists

partition splits a collection into a tuple of (matching, non-matching):

scala
val (evens, odds) = numbers.partition(_ % 2 == 0)
println(evens)  // List(2, 4)
println(odds)   // List(1, 3, 5)

zip and zipWithIndex

scala
val names = List("Alice", "Bob", "Carol")
val ages = List(30, 25, 35)

val pairs = names.zip(ages)
println(pairs)  // List((Alice,30), (Bob,25), (Carol,35))

val indexed = names.zipWithIndex
println(indexed)  // List((Alice,0), (Bob,1), (Carol,2))

Combining Operations

The real power comes from chaining operations:

scala
case class Transaction(user: String, amount: Double, category: String)

val transactions = List(
  Transaction("Alice", 50.0, "food"),
  Transaction("Bob", 200.0, "electronics"),
  Transaction("Alice", 30.0, "food"),
  Transaction("Carol", 150.0, "electronics"),
  Transaction("Bob", 10.0, "food"),
)

// Total spending per user, only for food category, users who spent > 20
val result = transactions
  .filter(_.category == "food")
  .groupBy(_.user)
  .map { case (user, txns) => user -> txns.map(_.amount).sum }
  .filter { case (_, total) => total > 20 }

println(result)  // Map(Alice -> 80.0, Bob -> 10.0) — wait, Bob is 10 which fails filter
// Actually: Map(Alice -> 80.0)

Frequently Asked Questions

Q: When should I use foldLeft vs reduce? Use foldLeft when you have an initial value (like 0 for sum, 1 for product, or an empty collection for building). Use reduce only when the collection is guaranteed non-empty and the operation makes sense without an initial value (like finding the max). In practice, foldLeft is safer and more flexible.

Q: Is flatMap just map followed by flatten? Yes — xs.flatMap(f) is equivalent to xs.map(f).flatten. The flatten operation takes a collection of collections and joins them into a single collection. Using flatMap directly is more efficient because it avoids creating the intermediate nested collection.

Q: Why does the order in foldLeft's function matter? In foldLeft(initial)(f), the function f takes (accumulator, element). In foldRight, it takes (element, accumulator). Getting the order wrong is a common bug — for example, foldRight(List.empty[Int])(_ :: _) preserves order, but foldRight(List.empty[Int])((acc, x) => acc :: x) would be a type error since you'd be prepending a list to an int.


Part of Scala Mastery Course — Module 12 of 22.