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 shapecase 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 matchingval 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 producesflatMap 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 entirelyval 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 emptyThis 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.
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 valueval 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) // 120Both 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 errorList.empty[Int].reduce((acc, x) => acc + x) // throws UnsupportedOperationException: empty.reduceThis 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 rightnumbers.foldRight("")((x, acc) => x + acc) // "1234" โ same result here, but argument order is reversed
// Where direction genuinely changes the result โ subtraction is not commutativenumbers.foldLeft(0)((acc, x) => acc - x) // ((((0-1)-2)-3)-4) = -10numbers.foldRight(0)((x, acc) => x - acc) // (1-(2-(3-(4-0)))) = -2fold (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 reportval 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.0val 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 functionnumbers.distinct // List(3, 1, 4, 5, 9, 2, 6) โ duplicates removed, order preservednumbers.take(3) // List(3, 1, 4) โ first 3 elementsnumbers.drop(3) // List(1, 5, 9, 2, 6) โ everything after the first 3numbers.find(_ > 4) // Some(5) โ first matching element, or Nonenumbers.exists(_ > 8) // true โ at least one element matchesnumbers.forall(_ > 0) // true โ every element matchesnumbers.zip(List("a","b","c")) // List((3,"a"), (1,"b"), (4,"c")) โ pairs up two collectionsnumbers.zipWithIndex // List((3,0), (1,1), (4,2), ...) โ pairs each element with its indexOperations Quick Reference
| Operation | Input โ Output | Use it for |
|---|---|---|
map | n elements โ n elements, transformed | 1-to-1 transformation |
filter | n elements โ โคn elements | Keeping a subset matching a condition |
flatMap | n elements โ any number, flattened | Transformations that themselves produce collections/Options |
fold / reduce | n elements โ 1 value | Combining everything into a single result |
groupBy | n elements โ a Map of buckets | Categorizing/bucketing by a key |
partition | n elements โ 2 collections | Splitting 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.