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.