1. Introduction
In this tutorial, we’ll take a look at for loops in Scala and their diverse feature set.
2. For Loops
Simply put, a for loop is a control flow statement. It allows executing some code repeatedly. We can use it when we need to repeat a code execution.
The general syntax of a for loop is:
for ( receiver <- generator ) {
statement
}
where:
- receiver is the variable that receives the next new value from the generator on each iteration, and
- generator, in the common cases, is a Range, Collection, or a Map – we’ll look at more advanced cases later
2.1. Using Ranges
To control how many times we repeat a loop, we can use a Range. This will also give us a loop counter.
A Range is an ordered sequence of Int values, defined by a starting and ending value:
val range = 1 to 3
val rangeUntil = 1 until 3
The keyword to defines a Range from the initial value to the ending one inclusively:
for (num <- range) {
println(num)
}
And if we ran the above, we’d see:
1
2
3
The keyword until defines the same Range, but without the ending value:
for (num <- rangeUntil) {
println(num)
}
which gives us a slightly different output:
1
2
On each iteration, the variable num (our loop counter) will receive the next value from the Range until the Range ends.
2.2. Multiple Generators
While in Java we’re forced to write nested loops, at the same time in Scala we can use only one for with multiple generators. We can show this using defined ranges from the previous example:
for {
i <- range
j <- rangeUntil
} {
println (s"$i, $j")
}
This gives us:
1, 1
1, 2
2, 1
2, 2
3, 1
3, 2
Please take a look at the syntax we used. If we use curly braces instead of parentheses we can skip semicolon (;) which is used to separate different generators.
Each additional generator adds one more inner iteration with its own loop counter. As a result, we have (range.size * rangeUntil.size = 6) iterations in total.
2.3. Using Collection
To iterate over any Collection, we can use the same syntax:
val colorList = Seq("R", "G", "B")
for (color <- colorList) {
println(color)
}
which will print each element of the Collection:
R
G
B
To demonstrate the use of multiple generators for Collection, let’s print all possible combinations of letters ‘R’, ‘G’, ‘B’ in a three-letter word, using the Seq we defined before**:**
for (c1 <- colorList; c2 <- colorList; c3 <- colorList) {
print(s"$c1$c2$c3 ")
}
This’ll result in all combinations being printed:
RRR RRG RRB RGR RGG RGB RBR RBG RBB GRR GRG GRB GGR GGG GGB GBR GBG GBB BRR BRG BRB BGR BGG BGB BBR BBG BBB
But, if we have one set of letters, the combinations like ‘RRR’ are not valid. In this case, we can use the guards.
2.4. Guards in a for Loop
Guard is just a condition we can use inside our for loop.
To filter-out invalid combinations, let’s add a guard to our previous code. We’ll also rewrite the for loop with the use of curly braces for more readability:
for {
c1 <- colorList
c2 <- colorList
if c2 != c1
c3 <- colorList
if c3 != c2 && c3 != c1
} {
print(s"$c1$c2$c3 ")
}
And we see that the output gets filtered:
RGB RBG GRB GBR BRG BGR
Every time c2 is filled with the next value, we can compare it with c1 to skip the next sub-iterations — that is, whenever c2 equals c1.
Also, we demonstrated that we can have as many guards as we want inside one for loop, and guards can be as complex as we want.
2.5. for Loops for Map
The difference with iterating over a Map is that the receiver will get a Tuple of two values – the key and the value associated with this key:
val map = Map("R" -> "Red", "G" -> "Green", "B" -> "Blue")
for ((key,value) <- map) {
println(s"""$key is for $value""")
}
And the result is:
R is for Red
G is for Green
B is for Blue
What if we have a Map of Lists*,* and we must iterate them both? We certainly can use multiple generators. In the outer iteration, we’ll get a key-value pair, and in the inner iteration, we will use a value, which is a List.
As an example, let’s say we have a deck of cards. It’s a Map, where keys are the cards’ suits and values are a List of ranks we have for each suit:
val deck = Map("♣" -> List("A", "K", "Q"),
"♦" -> List("J", "10"),
"♥" -> List("9", "8", "7"),
"♠" -> List("A", "K", "J", "6"))
We can print all the cards we have in the deck with just one for loop:
for {
(suit, cardList) <- deck
card <- cardList
} {
println(s"""$card of $suit""")
}
All the cards we have in a Map are printed out:
A of ♣
K of ♣
Q of ♣
J of ♦
10 of ♦
9 of ♥
8 of ♥
7 of ♥
A of ♠
K of ♠
J of ♠
6 of ♠
2.6. for Loop With yield
All for loop examples we’ve considered so far just execute a statement in each iteration.
But, whenever we need to transform each element in a collection into something new, we should use a special keyword yield in our for loop.
The syntax of the for loop will become:
val result = for ( generator ) yield {
yield_statement
}
yield will return a result of a statement execution as a new element of the resulting Collection, and we can use the result of the for loop and store it in a variable.
To demonstrate a for loop with yield, we’ll use a List of numbers:
val numberList = List(1, 2, 3)
Finally, with the help of a for loop, we will turn this List into the List of Strings:
val equation = for (number <- numberList) yield {
s"""$number + $number = ${number + number}"""
}
The resulting value is:
equation: List[String] = List(1 + 1 = 2, 2 + 2 = 4, 3 + 3 = 6)
The variable equation contains a List of Strings*,* each of which is a result of a running statement.
3. For-Comprehension
for with yield is a widely used tool in Scala, and it has another known name: for-comprehension. It could be applied for any container type that’s subject to similar conditions.
If any container type provides a map function for its elements, it can be used in a for-comprehension. To use this container type with multiple generators, it should also provide a flatMap function.
In Scala, all collections and maps provide implementations for map and flatMap functions*.*
In Category theory, such containers have a name — Monads. Scala language has other commonly known Monads: Option, Either, and Future. All of them implement both a map and flatMap function, so all of them can be used in a for-comprehension.
For example, let’s create two Option values for Int and for String.
val someIntValue = Some(10)
val someStringValue = Some("Ten")
We can use them in a for-comprehension with multiple generators because Option in Scala provides an implementation for both map and flatMap:
val result = for {
intValue <- someIntValue
stringValue <- someStringValue
} yield {
s"""$intValue is $stringValue"""
}
The type of the result of a for-comprehension will also be Option:
result: Option[String] = Some(10 is Ten)
To better understand how it works, we should know that a for-comprehension is only sugar for map and flatMap compositions.
We can rewrite the same example without for-comprehension:
val result = someIntValue.flatMap(intValue => someStringValue.map(stringValue => s"""$intValue is $stringValue"""))
We have the same result comparing to for-comprehension:
result: Option[String] = Some(10 is Ten)
4. Side-Effects
If repeated code inside an execution changes a state outside the loop, we’ll say that we have a “side-effect”. In our examples, printing is a side-effect, so our iterations are not pure.
Mapping each element of an iteration into something new without changing a state outside the loop is a pure iteration.
It’s good to understand the difference between iteration with side-effect and pure iteration. It is bad practice to mix both iterations together as it leads to hard-to-find errors in our code and poor testability. Also, iterations with side-effects are less readable.
5. Conclusion
In this tutorial, we learned how to use for loops in Scala.
We also looked at for-comprehensions; remember it’s just sugar for map and flatMap compositions.
As always, examples can be found over on GitHub. In the sample, take a closer look at the way we are dealing with side-effects in testing.