For loop was maybe the most used iterative structure in Java before the introduction of lambda expressions. With this loop we're able to write everything - starting with a simple mapping and terminating with a more complex "find-first element in the collection" feature. In Scala we can made these operations with monads and despite that, for loop is a feature offering a lot of possibilities.
A virtual conference at the intersection of Data and AI. This is not a conference for the hype. Its real users talking about real experiences.
- 40+ speakers with the likes of Hannes from Duck DB, Sol Rashidi, Joe Reis, Sadie St. Lawrence, Ryan Wolf from nvidia, Rebecca from lidl
- 12th September 2024
- Three simultaneous tracks
- Panels, Lighting Talks, Keynotes, Booth crawls, Roundtables and Entertainment.
- Topics include (ingestion, finops for data, data for inference (feature platforms), data for ML observability
- 100% virtual and 100% free
👉 Register here
This post talks about for loops in Scala. The first section presents the syntax and some general information. The second one lists what we can do with the loop and gives an example for each point. The last section mentions recursion that can be used instead of for loops.
For loop and Scala
Scala's for loop definition is very similar to a foreach loop because it clearly suggests the iteration over a collection of items. From the big picture the syntax of this loop looks like for (myItem <- expression) where expression is usually a collection or a range, if we want to access the collection elements by index.
Right now an important point to notice is the lack of native support for break keyword, able in other languages as Java to stop the iteration. Another not supported operation is the one expressed with continue, used to skip loop execution for given criteria.
For loop features
Among the features of for loop in Scala we can distinguish:
- comprehension - similarly to Python, Scala offers a lightweight syntax transforming sequences to another type. This notation is composed of a for loop followed by a yield keyword with transformation to apply:
it should "use comprehension as map function alternative" in { case class Number(value: Int) var generationStartTime: Option[Long] = None val wrappedNumbers = for (nr <- 1 to 10) yield { if (generationStartTime.isEmpty) generationStartTime = Some(System.currentTimeMillis()) Number(nr) } // Please notice that `yield` doesn't introduce any laziness about the execution - considering it as an alternative // to map helps to avoid the confusion. But to show that, we compare the first generation time that, if the generation // was lazy, it would be empty generationStartTime shouldBe defined wrappedNumbers should have size 10 wrappedNumbers should contain allOf(Number(1), Number(2), Number(3), Number(4), Number(5), Number(6), Number(7), Number(8), Number(9), Number(10)) }
- 2-loops in one iteration - nested for loops that in Java are known as for (...) { for (...) { // ...} . They are expressed much more shortly in Scala since we can write them as for (firstIteration; secondIteration,...):
it should "iterate over 2 lists" in { val upperCasedLetters = Seq("A", "B") val lowerCasedLetters = Seq("a", "b", "c") val constructedPairs = new mutable.ListBuffer[String]() // As you can see writing it like this is more concise than using 2 different for loops for (upperCased <- upperCasedLetters; lowerCased <- lowerCasedLetters) { constructedPairs.append(s"${upperCased}-${lowerCased}") } constructedPairs should have size 6 constructedPairs should contain allOf("A-a", "A-b", "A-c", "B-a", "B-b", "B-c") }
- guards - as in the case of Scala pattern matching, guards are used to exclude some values from the iteration, exactly as filter:
it should "use guard to eliminate not desired objects" in { // Here we simulate filter-map operation with the help of yield and guards val upperCasedLetters = Seq("A", "B") val lowerCasedLetters = Seq("a", "b", "c") // guards simulate not only filter(...) but also `continue` behavior used in other languages // to skip the execution for a specific condition val onlyMatchingPairs = for (upperCased <- upperCasedLetters; lowerCased <- lowerCasedLetters if lowerCased.toUpperCase == upperCased) yield { s"${upperCased}-${lowerCased}" } onlyMatchingPairs should have size 2 onlyMatchingPairs should contain allOf("A-a", "B-b") }
- generator-definition-filter pattern - with for loop we can also simulate behavior of filter-map operations. We achieve that by using a guard and defining the value we want to work on:
it should "use for variables inside the expression" in { case class Student(name: String, note: Int) val students = Seq(Student("student#1", 3), Student("student#2", 4), Student("student#3", 2), Student("student#4", 5)) // Here for loop is used with the "generator-definition-filter" pattern val notes = for ( student <- students; // generator note = student.note // definition if note > 2 // filter ) yield { s"Note=${note}" } notes should have size 3 notes should contain allOf("Note=3", "Note=4", "Note=5") }
- reverse iteration - reverse iteration can be achieved with simple for (item <- mySequence.reverse) { // ... but there is also a way similar to for-index-based loop. It uses to(end: Int, step: Int) method that in each iteration increases Range's number by the step value. Obviously, if the value is negative, the number will decrease:
it should "iterate from the end" in { val numbers = 1 to 10 // If we want to keep foreach-attitude we could use `.reverse for (i <- numbers.reverse)` // But this is another way to do for index-based collections val numbersFromTheEnd = new mutable.ListBuffer[Int]() for (index <- numbers.size-1 to (0, -1)) { val number = numbers(index) numbersFromTheEnd.append(number) } numbersFromTheEnd should have size 10 numbersFromTheEnd should contain allElementsOf(numbers.reverse) }
- "skipping" iteration - thanks to scala.collection.immutable.Range#by(step: Int) method we can pretty easily iterate every step elements, as shown in the following case:
it should "iterate every 2 element" in { val numbers = 1 to 10 val oddNumbers = new mutable.ListBuffer[Int]() for (index <- 0 to numbers.size-1 by 2) { oddNumbers.append(numbers(index)) } oddNumbers should have size 5 oddNumbers should contain allOf(1, 3, 5, 7, 9) }
- boolean flag-based breaks - as told, Scala's for loop doesn't provide break and continue keywords. But it's still possible to stop the loop execution after meeting the stop condition. One of valid solutions is the use of boolean flag:
it should "stop the execution after finding the first matching item thanks to a boolean flag" in { val numbers = 1 to 10 var wasFound = false var numberOfExecutions = 0 for (number <- numbers if !wasFound) { if (number == 5) { wasFound = true } numberOfExecutions += 1 } numberOfExecutions shouldBe 5 wasFound shouldBe true }
But notice however that it's still possible to use the pattern with Java's break. The difference is that in Scala the break is not a keyword but a method from scala.util.control.Breaks:it should "stop the execution with Breakable" in { val numbers = 1 to 10 var wasFound = false // This is only for illustration - the method with boolean flag seems to be more elegant than this one import scala.util.control.Breaks._ breakable { for (number <- numbers) { if (number == 5) { wasFound = true break() } // written with () to avoid confusion with break keyword } } wasFound shouldBe true }
For loop and recursion
A lot of programmers are familiar with for loops. However functional programmers (or at least some of them) tell that using for loops is not functional-friendly. It introduces mutability. Thus, the concept voted to replace them are recursion functions. After all they work purely on functions that in this paradigm are first-class citizens.
But recursion brings also some dangerous points. The first and maybe the most critical is the stack size. Each method has allocated a new stack frame and if the number of calls increases we can encounter a StackOverflow exception. Hopefully, to mitigate the issue, Scala proposes @tailrec annotation. The recursive methods annotated with it are translated at compilation time into loops. With that we can keep the functional style and avoid stack frames problem.
@tailrec is a constraint. It means that if a function can't be transformed the compilation error is produced:
Error:(162, 11) could not optimize @tailrec annotated method add: it is neither private nor final so can be overridden def add(nr: Int, limit: Int): Int = {
Recursion is also used in Scala library classes. For instance it's implemented for Stream's foreach method:
@tailrec override final def foreach[U](f: A => U) { if (!this.isEmpty) { f(head) tail.foreach(f) } }
And translating for loop into recursive function call is pretty straightforward:
"recursion" should "be able to replace for loop" in { // we can simulate for loop behavior with a recursion call @tailrec def findFirst(toFind: Int, items: Seq[Int]): Option[Int] = { val currentNumber = items.head if (currentNumber == toFind) { Some(currentNumber) } else if (items.size == 1) { None } else { findFirst(toFind, items.slice(1, items.size)) } } val number5 = findFirst(5, 1 to 10) number5 shouldBe defined number5.get shouldEqual 5 val number101 = findFirst(101, 1 to 10) number101 shouldBe empty }
Scala's for loop, as the whole language, mixes well procedural and functional style of writing. As shown in the first part, the loop resembles a lot to foreach loop where one item is read from collection eagerly. The eagerness doesn't change with comprehensions covered in the 2nd part, that are more like an alternative to map. But for the purists of functional programming, for loops are not completely good because they introduce mutability. It's why they can be replaced with recursion - even though under-the-hood it's converted into a loop with @tailrec annotation. It doesn't mean the for loops are bad and ugly. Their use depend a lot on coding style of the project. After all, that must be a pragmatic approach somewhere in the judgement.