Technology  /  Scala

🔴 Scala 21 guides · updated 2026

The JVM's hybrid OOP/functional language — immutability, pattern matching, and the type system that powers Spark and Akka, from first principles to production.

Collection Operations in Scala: map, filter, fold and reduce Explained

Once you know a collection’s shape (from the previous guide), the next question is always the same: how do you transform it? map, filter, fold, reduce, and flatMap cover the overwhelming majority of real transformations, and once they’re second nature, an explicit for loop with a mutable accumulator starts looking like what it usually is in Scala — a sign there’s a more direct, declarative way to say the same thing.


map — Transform Every Element, One to One

val numbers = List(1, 2, 3, 4, 5)
val doubled = numbers.map(x => x * 2) // List(2, 4, 6, 8, 10)
val squared = numbers.map(x => x * x) // List(1, 4, 9, 16, 25)
val asStrings = numbers.map(_.toString) // List("1", "2", "3", "4", "5")

map applies a function to every element and returns a new collection of the same length, with each element transformed independently — the output collection always has exactly as many elements as the input, one output per input, with no elements added, removed, or merged.

// map on a case class collection — the common real-world shape
case class Order(id: String, amount: Double)
val orders = List(Order("A1", 100.0), Order("A2", 200.0))
val ids = orders.map(_.id) // List("A1", "A2")
val withTax = orders.map(o => o.copy(amount = o.amount * 1.08))

filter — Keep Elements Matching a Condition

val numbers = List(1, 2, 3, 4, 5, 6)
val evens = numbers.filter(x => x % 2 == 0) // List(2, 4, 6)
val large = numbers.filter(_ > 3) // List(4, 5, 6)
val orders = List(Order("A1", 50.0), Order("A2", 250.0))
val largeOrders = orders.filter(_.amount > 100) // List(Order(A2,250.0))

filter returns a collection containing only the elements where the predicate function returns true — unlike map, the output can be shorter than the input (or empty), since elements failing the predicate are dropped entirely rather than transformed.

// filterNot — the inverse, keep elements NOT matching
val odds = numbers.filterNot(_ % 2 == 0) // List(1, 3, 5)
// partition — split into two collections in one pass: (matching, not matching)
val (evens2, odds2) = numbers.partition(_ % 2 == 0)
// evens2: List(2, 4, 6), odds2: List(1, 3, 5)

partition is worth knowing specifically because it avoids calling filter twice with inverse conditions when you need both groups — one traversal instead of two, and it’s a common enough real need (splitting valid/invalid records, active/inactive users) to have its own dedicated method.


flatMap — Transform and Flatten in One Step

val words = List("hello world", "scala is fun")
val allWords = words.flatMap(_.split(" "))
// List("hello", "world", "scala", "is", "fun") — NOT List(List("hello","world"), List("scala","is","fun"))
val nested = words.map(_.split(" ").toList)
// List(List("hello","world"), List("scala","is","fun")) — this IS what plain map produces

flatMap is exactly map followed by flattening one level of nesting — if your transformation function itself returns a collection (splitting a string into words, looking up zero-or-more related records), map alone would leave you with a collection of collections; flatMap merges them into a single flat collection automatically. This distinction — map preserves nesting, flatMap removes one level of it — is the single most common source of “why is my result a List[List[X]] instead of List[X]” confusion for developers new to the method.

// A very common real pattern: flatMap over Option to skip missing values entirely
val userIds = List("1", "2", "abc", "4")
def parseId(s: String): Option[Int] = s.toIntOption
val validIds = userIds.flatMap(parseId)
// List(1, 2, 4) — "abc" silently dropped, because parseId("abc") returns None, which flatMap treats as empty

This is a genuinely elegant, common idiom: Option behaves as a collection of zero or one elements for flatMap’s purposes — None flattens to nothing, Some(x) flattens to just x. This lets you parse/validate/look up a value per element and automatically discard the ones that failed, in a single expression, with no explicit if/isDefined checking needed anywhere.

flatMap flattens Option as a 0-or-1 element collection

List('1','2','abc','4')

map(parseId)

List(Some(1), Some(2), None, Some(4))

List(1, 2, 4)


fold and reduce — Combining Everything Into One Value

val numbers = List(1, 2, 3, 4, 5)
val sum = numbers.fold(0)((acc, x) => acc + x) // 15 — starts from an explicit initial value
val sumReduce = numbers.reduce((acc, x) => acc + x) // 15 — starts from the collection's own first element
val product = numbers.fold(1)((acc, x) => acc * x) // 120

Both fold and reduce combine every element into a single result using a binary combining function — the difference is where they start. fold takes an explicit initial value (which also determines the result type, and matters critically for an empty collection). reduce uses the collection’s own first element as the starting point, which means reduce throws on an empty collection (there’s no first element to start from), while fold handles an empty collection gracefully by simply returning the initial value untouched.

List.empty[Int].fold(0)((acc, x) => acc + x) // 0 — safe, no error
List.empty[Int].reduce((acc, x) => acc + x) // throws UnsupportedOperationException: empty.reduce

This is a real, practical reason to default to fold over reduce unless you’re certain the collection can never be empty — fold’s explicit initial value makes the empty-collection case well-defined rather than a runtime crash.

foldLeft vs foldRight — direction matters for non-commutative operations

val numbers = List(1, 2, 3, 4)
numbers.foldLeft("")((acc, x) => acc + x) // "1234" — processes left to right
numbers.foldRight("")((x, acc) => x + acc) // "1234" — same result here, but argument order is reversed
// Where direction genuinely changes the result — subtraction is not commutative
numbers.foldLeft(0)((acc, x) => acc - x) // ((((0-1)-2)-3)-4) = -10
numbers.foldRight(0)((x, acc) => x - acc) // (1-(2-(3-(4-0)))) = -2

fold (without direction) assumes the combining operation is associative and doesn’t matter which order elements are combined in — for operations where order genuinely matters (subtraction, string concatenation in a specific order, building up a structure that cares about sequence), foldLeft/foldRight make the direction explicit and, critically, note the argument order flips between them: foldLeft’s function receives (accumulator, element), while foldRight’s receives (element, accumulator) — a common source of subtly wrong results when switching between the two without noticing the swap.


Combining These Into Real Data Processing

case class Transaction(category: String, amount: Double)
val transactions = List(
Transaction("food", 45.0),
Transaction("transport", 20.0),
Transaction("food", 30.0),
Transaction("entertainment", 60.0),
Transaction("transport", 15.0)
)
// groupBy — bucket elements by a key, producing a Map[key, List[element]]
val byCategory = transactions.groupBy(_.category)
// Map("food" -> List(Transaction(food,45.0), Transaction(food,30.0)), "transport" -> List(...), ...)
// Chaining map/filter/fold into a real report
val totalsByCategory = byCategory.map { case (category, txns) =>
category -> txns.map(_.amount).sum
}
// Map("food" -> 75.0, "transport" -> 35.0, "entertainment" -> 60.0)
val totalSpend = transactions.map(_.amount).sum // 170.0
val bigTransactions = transactions.filter(_.amount > 40).map(_.category) // List("food", "entertainment")

This chained style — filter then map then sum/groupBy — is the standard shape of real Scala data-processing code, and it should look familiar if you’ve used Spark’s DataFrame API (covered in this site’s Spark guides) or SQL’s WHERE/SELECT/GROUP BY: the underlying transformation vocabulary is conceptually the same, because Spark’s Scala API is built directly on these exact collection operations, scaled up to distributed datasets.


collect — Filter and Map in a Single Pass

val values: List[Any] = List(1, "two", 3, "four", 5)
val onlyNumbers = values.collect { case n: Int => n }
// List(1, 3, 5) — filters to matching cases AND transforms, in one traversal
case class Order(id: String, amount: Double, status: String)
val orders = List(Order("A1", 100.0, "paid"), Order("A2", 50.0, "pending"), Order("A3", 200.0, "paid"))
val paidAmounts = orders.collect { case Order(_, amount, "paid") => amount }
// List(100.0, 200.0)

collect takes a partial function (a pattern-match block that doesn’t need to handle every possible case) and applies it only where a case matches, discarding elements that don’t — functionally equivalent to filter followed by map, but in a single pass and often more readable when the “keep or discard” condition and the “how to transform it” are both naturally expressed as a pattern match. This is a genuinely common idiom once pattern matching (covered in the next-but-one guide) becomes familiar — collect is essentially “pattern-matching flatMap” for the common case of transforming and filtering simultaneously.

Other Frequently Used Operations

val numbers = List(3, 1, 4, 1, 5, 9, 2, 6)
numbers.sorted // List(1, 1, 2, 3, 4, 5, 6, 9)
numbers.sortBy(x => -x) // descending, via a key function
numbers.distinct // List(3, 1, 4, 5, 9, 2, 6) — duplicates removed, order preserved
numbers.take(3) // List(3, 1, 4) — first 3 elements
numbers.drop(3) // List(1, 5, 9, 2, 6) — everything after the first 3
numbers.find(_ > 4) // Some(5) — first matching element, or None
numbers.exists(_ > 8) // true — at least one element matches
numbers.forall(_ > 0) // true — every element matches
numbers.zip(List("a","b","c")) // List((3,"a"), (1,"b"), (4,"c")) — pairs up two collections
numbers.zipWithIndex // List((3,0), (1,1), (4,2), ...) — pairs each element with its index

Operations Quick Reference

OperationInput → OutputUse it for
mapn elements → n elements, transformed1-to-1 transformation
filtern elements → ≤n elementsKeeping a subset matching a condition
flatMapn elements → any number, flattenedTransformations that themselves produce collections/Options
fold / reducen elements → 1 valueCombining everything into a single result
groupByn elements → a Map of bucketsCategorizing/bucketing by a key
partitionn elements → 2 collectionsSplitting into matching/non-matching in one pass

Frequently Asked Questions

When should I reach for flatMap instead of map? Whenever your transformation function itself returns a collection (or an Option/Either) and you want the elements themselves flattened into the result, rather than ending up with a nested collection of collections.

Is fold or reduce the safer default? fold — it handles the empty-collection case explicitly via its initial value, while reduce throws on an empty collection since it has no element to start from. Reach for reduce only when you’re certain the collection is guaranteed non-empty.

Why does foldLeft’s combining function take (acc, element) while foldRight’s takes (element, acc)? This mirrors the actual direction of processing — foldLeft builds up the accumulator by combining it with elements moving left-to-right, so the accumulator naturally comes first; foldRight processes right-to-left, so the current element comes first, with the accumulator representing “everything already folded from the right.” Getting this argument order backwards is a common bug when switching between the two.

Are these operations lazy or eager? For strict collections (List, Vector, Set, Map), every operation covered here executes immediately and eagerly, producing a fully-realized new collection at each step. For LazyList or Iterator (not covered in depth here), the same operations are lazy — nothing computes until a terminal operation actually forces evaluation.

Do these methods work identically on Set and Map, not just List/Vector? Yes — map, filter, fold, and flatMap are defined generically across Scala’s collection hierarchy, so the same vocabulary applies whether you’re transforming a List, a Set, or a Map’s key-value pairs, with only minor differences in what each operation’s function receives (a Map’s operations typically receive (key, value) tuples).


What’s Next

map, filter, fold, and flatMap are the transformation vocabulary you’ll reach for constantly — and flatMap’s behavior on Option previewed something bigger. Next: Option, Either and Try, covering how Scala represents “might be missing” and “might fail” without null or unchecked exceptions, and how flatMap chains them together cleanly.