Loops in Scala: for and while
Introduction
Iteration is a fundamental programming concept, but Scala approaches it differently than many imperative languages. While Scala supports traditional while
loops, it encourages a more functional approach through for
-comprehensions, which are more expressive and safer than traditional loops.
In this lesson, you'll learn both approaches to iteration in Scala, understand when to use each, and discover how for
-comprehensions can make your code more readable and less error-prone.
while Loops: The Traditional Approach
Basic while Loop
The while
loop in Scala works similarly to other languages:
var i = 0
while (i < 5) {
println(s"Iteration $i")
i += 1
}
// Prints: Iteration 0, Iteration 1, ..., Iteration 4
do-while Loop
Scala also supports do-while
loops:
var input = ""
do {
print("Enter 'quit' to exit: ")
input = scala.io.StdIn.readLine()
println(s"You entered: $input")
} while (input != "quit")
Practical while Loop Example
import scala.util.Random
def findRandomTarget(target: Int): Int = {
val random = new Random()
var attempts = 0
var current = 0
while (current != target) {
current = random.nextInt(10) + 1 // Random number 1-10
attempts += 1
println(s"Attempt $attempts: Generated $current")
}
println(s"Found $target after $attempts attempts!")
attempts
}
findRandomTarget(7)
for-Comprehensions: The Scala Way
For-comprehensions are Scala's idiomatic way to iterate. They're more powerful and safer than traditional loops.
Basic for Loop
// Simple range iteration
for (i <- 1 to 5) {
println(s"Number: $i")
}
// Using until (exclusive end)
for (i <- 1 until 5) {
println(s"Number: $i") // Prints 1, 2, 3, 4
}
// Iterating over collections
val fruits = List("apple", "banana", "cherry")
for (fruit <- fruits) {
println(s"I like $fruit")
}
for with yield: Generating New Collections
The real power of for
-comprehensions comes with yield
:
val numbers = 1 to 10
// Transform elements
val squares = for (n <- numbers) yield n * n
println(squares) // Vector(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)
// Work with strings
val names = List("alice", "bob", "charlie")
val capitalized = for (name <- names) yield name.capitalize
println(capitalized) // List(Alice, Bob, Charlie)
// More complex transformations
val words = List("hello", "world", "scala")
val lengths = for (word <- words) yield s"$word has ${word.length} letters"
println(lengths)
// List(hello has 5 letters, world has 5 letters, scala has 5 letters)
Guards: Filtering with if
Add conditions to filter elements:
val numbers = 1 to 20
// Filter even numbers
val evenNumbers = for (n <- numbers if n % 2 == 0) yield n
println(evenNumbers) // Vector(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
// Multiple conditions
val specialNumbers = for (
n <- numbers
if n % 2 == 0 // even
if n > 10 // greater than 10
) yield n
println(specialNumbers) // Vector(12, 14, 16, 18, 20)
// Filter and transform
val words = List("apple", "banana", "apricot", "cherry", "avocado")
val aWords = for (
word <- words
if word.startsWith("a")
) yield word.toUpperCase
println(aWords) // List(APPLE, APRICOT, AVOCADO)
Nested Loops
For-comprehensions handle nested iteration elegantly:
// Traditional nested loops equivalent
val coordinates = for (
x <- 1 to 3
y <- 1 to 3
) yield (x, y)
println(coordinates)
// Vector((1,1), (1,2), (1,3), (2,1), (2,2), (2,3), (3,1), (3,2), (3,3))
// Multiplication table
val multiplicationTable = for (
i <- 1 to 5
j <- 1 to 5
) yield s"$i × $j = ${i * j}"
multiplicationTable.foreach(println)
Variable Binding in for-Comprehensions
You can introduce intermediate variables:
val words = List("hello", "world", "scala", "programming")
val analysis = for (
word <- words
length = word.length // Bind intermediate value
if length > 4
) yield s"$word ($length chars)"
println(analysis)
// List(hello (5 chars), world (5 chars), scala (5 chars), programming (11 chars))
// Multiple bindings
val numbers = 1 to 10
val calculations = for (
n <- numbers
doubled = n * 2
squared = n * n
if doubled < squared
) yield s"$n: doubled=$doubled, squared=$squared"
println(calculations)
Working with Different Collection Types
Lists and Arrays
val fruits = List("apple", "banana", "cherry")
val numbers = Array(1, 2, 3, 4, 5)
// Lists
val fruitLengths = for (fruit <- fruits) yield fruit.length
println(fruitLengths) // List(5, 6, 6)
// Arrays
val evenNumbers = for (n <- numbers if n % 2 == 0) yield n
println(evenNumbers.toList) // List(2, 4)
// Convert between types
val listFromArray = for (n <- numbers) yield n * 10
println(listFromArray.getClass) // class [I (Array)
Ranges
// Different range types
val range1 = 1 to 10 // 1, 2, 3, ..., 10
val range2 = 1 until 10 // 1, 2, 3, ..., 9
val range3 = 1 to 10 by 2 // 1, 3, 5, 7, 9
// Reverse ranges
val countdown = for (i <- 10 to 1 by -1) yield i
println(countdown) // Vector(10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
// Character ranges
val alphabet = for (c <- 'a' to 'z') yield c
println(alphabet.mkString) // abcdefghijklmnopqrstuvwxyz
Maps
val grades = Map("Alice" -> 95, "Bob" -> 87, "Charlie" -> 92)
// Iterate over map entries
for ((name, grade) <- grades) {
println(s"$name scored $grade")
}
// Filter and transform
val highPerformers = for (
(name, grade) <- grades
if grade >= 90
) yield s"$name (${grade}%)"
println(highPerformers) // Set(Alice (95%), Charlie (92%))
Advanced for-Comprehension Patterns
Pattern Matching in for-Comprehensions
case class Person(name: String, age: Int, city: String)
val people = List(
Person("Alice", 30, "New York"),
Person("Bob", 25, "San Francisco"),
Person("Charlie", 35, "New York"),
Person("Diana", 28, "Chicago")
)
// Extract values using pattern matching
val newYorkers = for (
Person(name, age, "New York") <- people
) yield s"$name ($age)"
println(newYorkers) // List(Alice (30), Charlie (35))
// Partial pattern matching with guards
val youngAdults = for (
Person(name, age, city) <- people
if age < 30
) yield s"$name from $city"
println(youngAdults) // List(Bob from San Francisco, Diana from Chicago)
Working with Options
val maybeNumbers = List(Some(1), None, Some(3), Some(4), None)
// Extract values from Some
val actualNumbers = for (
Some(number) <- maybeNumbers
) yield number
println(actualNumbers) // List(1, 3, 4)
// More complex example
case class User(id: Int, name: String, email: Option[String])
val users = List(
User(1, "Alice", Some("alice@example.com")),
User(2, "Bob", None),
User(3, "Charlie", Some("charlie@test.com"))
)
val emailList = for (
User(id, name, Some(email)) <- users
) yield s"$name <$email>"
println(emailList) // List(Alice <alice@example.com>, Charlie <charlie@test.com>)
Flattening Nested Collections
val nestedNumbers = List(List(1, 2), List(3, 4, 5), List(6))
// Flatten and transform
val allNumbers = for (
innerList <- nestedNumbers
number <- innerList
) yield number * 2
println(allNumbers) // List(2, 4, 6, 8, 10, 12)
// Real-world example: processing grouped data
case class Department(name: String, employees: List[String])
val departments = List(
Department("Engineering", List("Alice", "Bob", "Charlie")),
Department("Marketing", List("Diana", "Eve")),
Department("Sales", List("Frank", "Grace", "Henry"))
)
val allEmployees = for (
dept <- departments
employee <- dept.employees
) yield s"$employee (${dept.name})"
allEmployees.foreach(println)
Practical Examples
Example 1: Data Processing Pipeline
case class Sale(product: String, quantity: Int, price: Double, region: String)
val sales = List(
Sale("Laptop", 5, 999.99, "North"),
Sale("Mouse", 20, 25.50, "South"),
Sale("Keyboard", 15, 75.00, "North"),
Sale("Monitor", 8, 299.99, "East"),
Sale("Laptop", 3, 999.99, "South")
)
// Complex data processing with for-comprehension
val report = for (
sale <- sales
revenue = sale.quantity * sale.price
if revenue > 1000 // Only high-value sales
if sale.region == "North" || sale.region == "South"
) yield {
val formatted = f"${sale.product}%-10s: ${sale.quantity}%3d units, $$${revenue}%8.2f"
(sale.region, formatted)
}
// Group by region
val grouped = report.groupBy(_._1).view.mapValues(_.map(_._2))
grouped.foreach { case (region, sales) =>
println(s"\n$region Region:")
sales.foreach(sale => println(s" $sale"))
}
Example 2: Game Board Generation
sealed trait Cell
case object Empty extends Cell
case object Wall extends Cell
case object Player extends Cell
def generateMaze(width: Int, height: Int): Array[Array[Cell]] = {
val maze = Array.ofDim[Cell](height, width)
// Initialize with walls
for (
row <- 0 until height
col <- 0 until width
) {
maze(row)(col) = if (row == 0 || row == height - 1 ||
col == 0 || col == width - 1) Wall else Empty
}
// Add some internal walls
for (
row <- 2 until height - 2 by 2
col <- 2 until width - 2 by 2
if scala.util.Random.nextBoolean()
) {
maze(row)(col) = Wall
}
// Place player
maze(1)(1) = Player
maze
}
def printMaze(maze: Array[Array[Cell]]): Unit = {
for (row <- maze) {
val line = for (cell <- row) yield cell match {
case Empty => " "
case Wall => "#"
case Player => "@"
}
println(line.mkString)
}
}
val maze = generateMaze(15, 10)
printMaze(maze)
Example 3: Text Analysis
def analyzeText(text: String): Map[String, Any] = {
val words = text.toLowerCase.split("\\W+").filter(_.nonEmpty)
val wordFrequency = for (
word <- words.distinct
) yield word -> words.count(_ == word)
val longWords = for (
word <- words.distinct
if word.length > 5
) yield word
val vowelCounts = for (
word <- words.distinct
vowelCount = word.count("aeiou".contains(_))
) yield word -> vowelCount
Map(
"totalWords" -> words.length,
"uniqueWords" -> words.distinct.length,
"averageLength" -> words.map(_.length).sum.toDouble / words.length,
"longWords" -> longWords.sorted,
"mostCommon" -> wordFrequency.maxBy(_._2),
"vowelRich" -> vowelCounts.filter(_._2 >= 3).sortBy(-_._2)
)
}
val sampleText = """
Scala is a powerful programming language that combines object-oriented
and functional programming paradigms. It runs on the Java Virtual Machine
and provides excellent interoperability with Java libraries.
"""
val analysis = analyzeText(sampleText)
analysis.foreach { case (key, value) =>
println(s"$key: $value")
}
Performance Considerations
for-Comprehensions vs Traditional Loops
import scala.collection.mutable.ArrayBuffer
// Traditional imperative approach
def processImperative(numbers: List[Int]): List[Int] = {
val result = ArrayBuffer[Int]()
var i = 0
while (i < numbers.length) {
val n = numbers(i)
if (n % 2 == 0 && n > 10) {
result += n * n
}
i += 1
}
result.toList
}
// Functional approach with for-comprehension
def processFunctional(numbers: List[Int]): List[Int] = {
for (
n <- numbers
if n % 2 == 0
if n > 10
) yield n * n
}
// Both approaches are equivalent but functional is more readable
val numbers = (1 to 100).toList
println(processImperative(numbers))
println(processFunctional(numbers))
Choosing the Right Approach
// Use while loops for:
// - Performance-critical code where you need precise control
// - Stateful operations that don't map well to functional style
def findFirstOccurrence(text: String, pattern: String): Int = {
var index = 0
while (index <= text.length - pattern.length) {
if (text.substring(index, index + pattern.length) == pattern) {
return index
}
index += 1
}
-1
}
// Use for-comprehensions for:
// - Data transformation and filtering
// - Working with collections
// - When readability is important
def extractEmailDomains(emails: List[String]): List[String] = {
for (
email <- emails
if email.contains("@")
domain = email.split("@")(1)
if domain.contains(".")
) yield domain.toLowerCase
}
Common Patterns and Idioms
Generating Combinations
// Generate all pairs
val numbers = List(1, 2, 3, 4)
val pairs = for (
i <- numbers
j <- numbers
if i < j
) yield (i, j)
println(pairs) // List((1,2), (1,3), (1,4), (2,3), (2,4), (3,4))
// Generate Cartesian product
val colors = List("red", "green", "blue")
val sizes = List("small", "medium", "large")
val products = for (
color <- colors
size <- sizes
) yield s"$size $color"
println(products)
Conditional Processing
def processData(data: List[Int], condition: String): List[Int] = {
for (
n <- data
result <- condition match {
case "double" => Some(n * 2)
case "square" => Some(n * n)
case "positive" => if (n > 0) Some(n) else None
case _ => None
}
) yield result
}
println(processData(List(-2, -1, 0, 1, 2), "positive")) // List(1, 2)
println(processData(List(1, 2, 3), "square")) // List(1, 4, 9)
Best Practices
1. Prefer for-Comprehensions for Collection Processing
// Good: Readable and functional
val processed = for (
item <- items
if item.isValid
transformed = item.transform()
if transformed.nonEmpty
) yield transformed
// Avoid: Imperative style when functional is clearer
var result = List.empty[String]
for (item <- items) {
if (item.isValid) {
val transformed = item.transform()
if (transformed.nonEmpty) {
result = result :+ transformed
}
}
}
2. Use Guards for Complex Filtering
// Good: Clear intention
val validUsers = for (
user <- users
if user.age >= 18
if user.isActive
if user.email.isDefined
) yield user
// Less clear: Combining conditions
val validUsers2 = for (
user <- users
if user.age >= 18 && user.isActive && user.email.isDefined
) yield user
3. Break Complex Operations into Steps
// Good: Clear steps
val result = for (
data <- rawData
cleaned = data.trim.toLowerCase
if cleaned.nonEmpty
parsed = parseData(cleaned)
if parsed.isSuccess
) yield parsed.get
// Avoid: Everything in one expression
val result2 = for (
data <- rawData
if data.trim.toLowerCase.nonEmpty && parseData(data.trim.toLowerCase).isSuccess
) yield parseData(data.trim.toLowerCase).get
Summary
In this lesson, you've learned about iteration in Scala:
✅ while Loops: Traditional imperative iteration for specific use cases
✅ for-Comprehensions: Scala's functional approach to iteration
✅ yield: Generating new collections from existing ones
✅ Guards: Filtering elements with conditions
✅ Pattern Matching: Destructuring data in loops
✅ Nested Iteration: Handling multiple collections elegantly
✅ Best Practices: When to use each approach for maximum clarity
For-comprehensions are one of Scala's most elegant features, making collection processing more readable and less error-prone than traditional loops. They're a stepping stone to understanding more advanced functional programming concepts.
What's Next
In the next lesson, we'll explore "The Scala Toolkit: Your First Essential Library." You'll get an introduction to the Scala Toolkit, a curated set of libraries for common tasks like reading files, parsing JSON, and making HTTP requests.
This will be your first taste of Scala's rich ecosystem and how it makes common programming tasks easier and more enjoyable.
Ready to expand your toolkit? Let's continue!
Comments
Be the first to comment on this lesson!