Scala - reduce and fold Operations

The `reduce` operation processes a collection by repeatedly applying a binary function to combine elements. It takes the first element as the initial accumulator and applies the function to...

Key Insights

  • reduce and fold are fundamental collection operations that aggregate elements into a single value, but fold provides an initial value while reduce uses the first element, making fold safer for empty collections
  • foldLeft and foldRight differ in both traversal direction and associativity, with foldLeft being tail-recursive and preferred for performance on large collections
  • Understanding the accumulator pattern in these operations is critical for functional programming, enabling elegant solutions for summation, product calculation, collection transformation, and complex data aggregation

Understanding reduce Operations

The reduce operation processes a collection by repeatedly applying a binary function to combine elements. It takes the first element as the initial accumulator and applies the function to subsequent elements.

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

// Basic reduction - sum
val sum = numbers.reduce((acc, n) => acc + n)
println(sum) // 15

// Shortened syntax
val sumShort = numbers.reduce(_ + _)
println(sumShort) // 15

// Product
val product = numbers.reduce(_ * _)
println(product) // 120

The critical limitation of reduce is its behavior with empty collections:

val emptyList = List[Int]()
// val result = emptyList.reduce(_ + _) // Throws UnsupportedOperationException

// Use reduceOption for safety
val safeResult = emptyList.reduceOption(_ + _)
println(safeResult) // None

val nonEmptyResult = numbers.reduceOption(_ + _)
println(nonEmptyResult) // Some(15)

fold Operations: The Safe Alternative

fold operations require an initial value, making them safe for empty collections and providing more control over the accumulation process.

val numbers = List(1, 2, 3, 4, 5)
val emptyList = List[Int]()

// fold with initial value
val sum = numbers.fold(0)(_ + _)
println(sum) // 15

// Safe with empty collections
val emptySum = emptyList.fold(0)(_ + _)
println(emptySum) // 0

// Different initial value
val sumPlusTen = numbers.fold(10)(_ + _)
println(sumPlusTen) // 25

The initial value’s type determines the result type, enabling transformations:

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

// Accumulate into a string
val stringResult = numbers.fold("")((acc, n) => acc + n.toString)
println(stringResult) // "12345"

// Build a different structure
val doubled = numbers.fold(List[Int]())((acc, n) => acc :+ (n * 2))
println(doubled) // List(2, 4, 6, 8, 10)

foldLeft vs foldRight: Direction Matters

foldLeft processes elements from left to right, while foldRight processes from right to left. This distinction affects both performance and results.

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

// foldLeft - left to right
val leftResult = numbers.foldLeft(0) { (acc, n) =>
  println(s"acc: $acc, n: $n")
  acc + n
}
// Output order: (0,1), (1,2), (3,3), (6,4), (10,5)
// Result: 15

// foldRight - right to left
val rightResult = numbers.foldRight(0) { (n, acc) =>
  println(s"n: $n, acc: $acc")
  n + acc
}
// Output order: (5,0), (4,5), (3,9), (2,12), (1,14)
// Result: 15

Notice the parameter order difference: foldLeft takes (accumulator, element) while foldRight takes (element, accumulator).

For non-commutative operations, direction produces different results:

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

// Division is not commutative
val leftDiv = numbers.foldLeft(64.0)(_ / _)
println(leftDiv) // ((((64 / 1) / 2) / 3) / 4) = 2.666...

val rightDiv = numbers.foldRight(64.0)(_ / _)
println(rightDiv) // (1 / (2 / (3 / (4 / 64)))) = 0.09375

// String concatenation
val words = List("Scala", "is", "powerful")
val leftString = words.foldLeft("")((acc, word) => acc + " " + word)
println(leftString) // " Scala is powerful"

val rightString = words.foldRight("")((word, acc) => word + " " + acc)
println(rightString) // "Scala is powerful "

Performance Considerations

foldLeft is tail-recursive and optimized by the Scala compiler, making it more efficient for large collections:

// foldLeft is tail-recursive - safe for large collections
def sumLarge(numbers: List[Int]): Int = {
  numbers.foldLeft(0)(_ + _)
}

// foldRight is not tail-recursive - can cause stack overflow
def sumLargeRight(numbers: List[Int]): Int = {
  numbers.foldRight(0)(_ + _)
}

val largeList = (1 to 100000).toList
println(sumLarge(largeList)) // Works fine

// sumLargeRight may cause StackOverflowError on very large lists

Use foldLeft as the default choice unless you specifically need right-to-left processing.

Practical Applications

Grouping and Counting

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

val transactions = List(
  Transaction("food", 50.0),
  Transaction("transport", 30.0),
  Transaction("food", 70.0),
  Transaction("entertainment", 100.0),
  Transaction("transport", 20.0)
)

// Sum by category
val categoryTotals = transactions.foldLeft(Map[String, Double]()) { (acc, tx) =>
  acc + (tx.category -> (acc.getOrElse(tx.category, 0.0) + tx.amount))
}
println(categoryTotals)
// Map(food -> 120.0, transport -> 50.0, entertainment -> 100.0)

// Count occurrences
val categoryCounts = transactions.foldLeft(Map[String, Int]()) { (acc, tx) =>
  acc + (tx.category -> (acc.getOrElse(tx.category, 0) + 1))
}
println(categoryCounts)
// Map(food -> 2, transport -> 2, entertainment -> 1)

Building Complex Structures

case class Person(name: String, age: Int, department: String)

val people = List(
  Person("Alice", 30, "Engineering"),
  Person("Bob", 25, "Sales"),
  Person("Charlie", 35, "Engineering"),
  Person("Diana", 28, "Sales")
)

// Group by department with average age
val deptStats = people.foldLeft(Map[String, (Int, Int)]()) { (acc, person) =>
  val (count, totalAge) = acc.getOrElse(person.department, (0, 0))
  acc + (person.department -> (count + 1, totalAge + person.age))
}.map { case (dept, (count, total)) =>
  dept -> total.toDouble / count
}

println(deptStats)
// Map(Engineering -> 32.5, Sales -> 26.5)

Validation and Error Accumulation

sealed trait ValidationResult
case class Valid(value: Int) extends ValidationResult
case class Invalid(errors: List[String]) extends ValidationResult

def validatePositive(n: Int): ValidationResult =
  if (n > 0) Valid(n) else Invalid(List(s"$n is not positive"))

val inputs = List(5, -3, 10, -1, 7)

val validationResult = inputs.foldLeft[ValidationResult](Valid(0)) {
  case (Valid(sum), n) =>
    validatePositive(n) match {
      case Valid(v) => Valid(sum + v)
      case Invalid(errs) => Invalid(errs)
    }
  case (Invalid(errors), n) =>
    validatePositive(n) match {
      case Valid(_) => Invalid(errors)
      case Invalid(newErrs) => Invalid(errors ++ newErrs)
    }
}

println(validationResult)
// Invalid(List(-3 is not positive, -1 is not positive))

Advanced Patterns

Custom Fold Operations

// Implement map using foldLeft
def customMap[A, B](list: List[A])(f: A => B): List[B] =
  list.foldLeft(List[B]())((acc, elem) => acc :+ f(elem))

println(customMap(List(1, 2, 3))(_ * 2)) // List(2, 4, 6)

// Implement filter using foldLeft
def customFilter[A](list: List[A])(predicate: A => Boolean): List[A] =
  list.foldLeft(List[A]())((acc, elem) =>
    if (predicate(elem)) acc :+ elem else acc
  )

println(customFilter(List(1, 2, 3, 4, 5))(_ % 2 == 0)) // List(2, 4)

// Implement reverse using foldLeft
def customReverse[A](list: List[A]): List[A] =
  list.foldLeft(List[A]())((acc, elem) => elem :: acc)

println(customReverse(List(1, 2, 3, 4, 5))) // List(5, 4, 3, 2, 1)

Combining Multiple Operations

case class Order(id: Int, items: List[String], total: Double, isPaid: Boolean)

val orders = List(
  Order(1, List("book", "pen"), 25.0, true),
  Order(2, List("laptop"), 1200.0, false),
  Order(3, List("mouse", "keyboard"), 80.0, true),
  Order(4, List("monitor"), 300.0, true)
)

// Calculate total revenue from paid orders with discounts
val revenue = orders.foldLeft(0.0) { (acc, order) =>
  if (order.isPaid) {
    val discount = if (order.total > 100) 0.9 else 1.0
    acc + (order.total * discount)
  } else acc
}

println(f"Total revenue: $$${revenue}%.2f") // Total revenue: $377.00

These operations form the backbone of functional data processing in Scala. Master them to write concise, expressive code that handles complex aggregations with clarity and type safety.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.