1. Introduction

When working with functional programming languages like Scala, it’s common to have a variety of high-order functions to process collections, including reduceLeft, reduceRight, foldLeft, foldRight, scanLeft, and scanRight. Each of these functions combines the elements of a collection into a final result. However, they differ in how they iterate through the collection and how they compile the final result.

In this quick tutorial, we’ll learn about these six important functions of Scala. We will compare and contrast their different behaviors and provide some code examples to illustrate their use cases.

2. reduceLeft and reduceRight

The reduceLeft combines the elements of a collection by successively applying a binary operator from left to right and produces a single result. It applies the binary operation to the first two elements, then applies the outcome to the third element, and so on. It reduces the collection to a single value:

"reduceLeft" should "calculate the sum correctly" in {
  val numbers = List(1, 2, 3, 4, 5)
  val actualSum = numbers.reduceLeft(_ + _)
  // op: 1 + 2 = 3
  // op: 3 + 3 = 6
  // op: 6 + 4 = 10
  // op: 10 + 5 = 15
  // res: Int = 15
  assert(15 == actualSum)
}

Similarly, we can find the largest number or concatenate a List of Strings:

"reduceLeft" should "find the largest number correctly" in {
  val numbers = List(10, 22, 5, 71, 43)
  val actualResult = numbers.reduceLeft(_ max _)
  assert(71 == actualResult)
}
"reduceLeft" should "concatenate strings correctly" in {
  val alphabets = List("A", "B", "C", "D", "E")
  val actualResult = alphabets.reduceLeft(_ + _)
  assert("ABCDE" == actualResult)
}

The reduceRight function operates in the same manner as reduceLeft, with the only difference being the direction. The reduceRight will apply the binary operation to the last two elements of the collection first, then the third-to-last two elements, and so on, until it reaches the first two elements:

"reduceRight" should "calculate the sum correctly" in {
  val numbers = List(1, 2, 3, 4, 5)
  val actualSum = numbers.reduceRight(_ + _)
  // op: 5 + 4 = 9
  // op: 9 + 3 = 12
  // op: 12 + 2 = 14
  // op: 14 + 1 = 15
  // res: Int = 15
  assert(15 == actualSum)
}

When the input collection is empty, reduceLeft and reduceRight throw an UnsupportedOperationException because there is nothing to reduce.

Here’s some code that illustrates the behavior:

"reduceLeft" should "throw an exception" in {
  val numbers = List.empty[Int]
  assertThrows[UnsupportedOperationException] {
    numbers.reduceLeft(_ max _)
  }
}

3. foldLeft and foldRight

The foldLeft and foldRight are similar to reduceLeft and reduceRight, respectively, but they take an initial value as a parameter.

In the foldLeft, the initial value will combine with the first element of the collection using a binary operator, and the result will combine with the second element, and so on:

"foldLeft" should "calculate the sum correctly" in {
  val numbers = List(1, 2, 3, 4, 5)
  val actualSum = numbers.foldLeft(5)(_ + _)
  // op: 5 + 1 = 6
  // op: 6 + 2 = 8
  // op: 8 + 3 = 11
  // op: 11 + 4 = 15
  // op: 15 + 5 = 20
  // res: Int = 20
  assert(20 == actualSum)
}

Similarly, in the foldRight, the initial value will combine with the last element, and the result will combine with the second-last element, and so on, until it reaches the first element:

"foldRight" should "concatenate the strings correctly" in {
  val alphabets = List("A", "B", "C", "D", "E")
  val actualResult = alphabets.foldRight("$")(_ + _)
  // op: E + $ = E$
  // op: D + E$ = DE$
  // op: C + DE$ = CDE$
  // op: B + CDE$ = BCDE$
  // op: A + BCDE$ = ABCDE$
  // res: String = ABCDE$
  assert("ABCDE$" == actualResult)
}

When the input collection is empty, the foldLeft and foldRight functions return a collection with only the initial element:

"foldRight" should "return the initial element i.e $" in {
  val alphabets = List.empty[String]
  val actualResult = alphabets.foldRight("$")(_ + _)
  assert("$" == actualResult)
}

4. scanLeft and scanRight

The scanLeft and scanRight are similar to foldLeft and foldRight, respectively, but they return a collection of intermediate results, not just the final result:

"scanLeft" should "have correct intermediate states" in {
  val numbers = List(1, 2, 3, 4, 5)
  val actualResult = numbers.scanLeft(1)(_ + _)
  assert(List(1, 2, 4, 7, 11, 16) == actualResult)
}

"scanRight" should "have correct intermediate states" in {
  val numbers = List(1, 2, 3, 4, 5)
  val actualResult = numbers.scanRight(1)(_ + _)
  assert(List(16, 15, 13, 10, 6, 1) == actualResult)
}

When the input collection is empty, the scanLeft and scanRight functions return a collection with only the initial element:

"scanRight" should "return the initial element i.e 5" in {
  val numbers = List.empty[Int]
  val actualResult = numbers.scanRight(5)(_ + _)
  assert(List(5) == actualResult)
}

5. When to Use Each

What function should be chosen over the other totally depends on the context. We can choose the ideal function for our needs by understanding their distinct behaviors.

Therefore, the following table can help us to find the most suitable function according to our requirements:

Function Output a Single Value Output Intermediate Results Accepts Initial Value When Collection is Empty Examples
reduceLeft Yes No No throws UnsupportedOperationException Find max/min in the list
reduceRight Yes No No throws UnsupportedOperationException String concatenation
foldLeft Yes No Yes returns a collection with only the initial element Tree traversal
foldRight Yes No Yes returns a collection with only the initial element File system traversal
scanLeft No Yes Yes returns a collection with only the initial element Inventory management
scanRight No Yes Yes returns a collection with only the initial element Statistical analysis

6. Conclusion

In this tutorial, we have learned about the six powerful functions of Scala language for aggregating the elements of a collection. These functions use different traversal orders and accumulation strategies to aggregate elements. By understanding the differences between these functions, we can choose the right one for our needs.

As usual, the full source code can be found over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.