Expressions and Control Structures: if/else
Introduction
One of the most fundamental concepts that distinguishes Scala from many other programming languages is that almost everything is an expression that returns a value. This is a stark contrast to languages where you have both statements (which perform actions) and expressions (which produce values).
In this lesson, we'll explore Scala's expression-oriented nature through the lens of conditional logic using if/else
constructs. You'll learn how this approach leads to more concise, readable, and functional code.
Statements vs Expressions
Before diving into Scala's if/else
, let's understand the difference:
- Statement: Performs an action, doesn't return a value
- Expression: Evaluates to produce a value
// In many languages, if is a statement:
// if (condition) doSomething(); // No value returned
// In Scala, if is an expression:
val result = if (condition) value1 else value2 // Returns a value
This expression-oriented approach enables:
- More concise code
- Better composability
- Functional programming patterns
- Fewer temporary variables
Basic if/else Expressions
Simple Conditional Assignment
val temperature = 25
val weather = if (temperature > 30) "hot" else "comfortable"
println(s"It's $weather today") // "It's comfortable today"
// Compare with statement-based approach:
// String weather;
// if (temperature > 30) {
// weather = "hot";
// } else {
// weather = "comfortable";
// }
if/else with Different Types
Since everything is an expression, the if
and else
branches must return compatible types:
val age = 17
val status = if (age >= 18) "adult" else "minor"
// Both branches return String, so status is String
val score = 85
val grade = if (score >= 90) 'A' else if (score >= 80) 'B' else 'C'
// All branches return Char, so grade is Char
// What happens with incompatible types?
val mixed = if (true) "hello" else 42
// Type is Any (the common supertype of String and Int)
if without else
When you omit the else
clause, Scala returns Unit
(similar to void
):
val result = if (temperature > 40) println("It's extremely hot!")
// result has type Unit
// This is equivalent to:
val result2 = if (temperature > 40) println("It's extremely hot!") else ()
Multi-line if/else Expressions
For complex logic, you can use blocks:
val grade = {
val score = 87
val attendance = 0.95
val participation = true
if (score >= 90 && attendance > 0.9) {
"A"
} else if (score >= 80 && attendance > 0.8) {
"B"
} else if (score >= 70) {
"C"
} else {
"F"
}
}
Nested if/else Expressions
You can nest conditional expressions for complex decision trees:
def categorizeTemperature(temp: Int, humidity: Int): String = {
if (temp > 30) {
if (humidity > 70) "hot and humid"
else "hot and dry"
} else if (temp > 20) {
if (humidity > 60) "warm and humid"
else "pleasant"
} else {
if (temp < 10) "cold"
else "cool"
}
}
val weather = categorizeTemperature(25, 50)
println(weather) // "pleasant"
Boolean Logic in Conditions
Scala supports all standard boolean operators:
val age = 25
val hasLicense = true
val hasInsurance = true
val hasGoodRecord = false
// Logical AND (&&)
val canRentCar = age >= 21 && hasLicense && hasInsurance
// true && true && true = true
// Logical OR (||)
val canDrive = hasLicense || age < 16 // Learning permit case
// true || false = true
// Logical NOT (!)
val needsTraining = !hasGoodRecord
// !false = true
// Complex conditions
val premium = if (age < 25 || !hasGoodRecord) {
"high"
} else if (age > 65) {
"senior"
} else {
"standard"
}
Short-Circuit Evaluation
Scala uses short-circuit evaluation for &&
and ||
:
def expensiveCheck(): Boolean = {
println("Performing expensive check...")
Thread.sleep(1000) // Simulate expensive operation
true
}
val quickResult = false && expensiveCheck()
// expensiveCheck() is never called because false && anything = false
val anotherResult = true || expensiveCheck()
// expensiveCheck() is never called because true || anything = true
Using if/else in Function Bodies
Since functions return the last expression, if/else
works naturally:
def absoluteValue(x: Int): Int =
if (x >= 0) x else -x
def sign(x: Int): String =
if (x > 0) "positive"
else if (x < 0) "negative"
else "zero"
def max(a: Int, b: Int): Int =
if (a >= b) a else b
// Using in more complex functions
def calculateDiscount(amount: Double, customerType: String): Double = {
val baseDiscount = if (amount > 1000) 0.1 else 0.05
val typeMultiplier = customerType.toLowerCase match {
case "premium" => 2.0
case "gold" => 1.5
case _ => 1.0
}
if (baseDiscount * typeMultiplier > 0.3) 0.3 else baseDiscount * typeMultiplier
}
Ternary Operator Alternative
Scala doesn't have a ternary operator (condition ? value1 : value2
), but if/else
serves the same purpose more readably:
// Java/C++ ternary:
// String result = (score >= 60) ? "pass" : "fail";
// Scala equivalent (more readable):
val result = if (score >= 60) "pass" else "fail"
// For simple cases, you can make it very concise:
val status = if (isActive) "ON" else "OFF"
val sign = if (number >= 0) "+" else "-"
Type Inference with Conditionals
Scala's type inference works well with conditional expressions:
// Type is inferred as String
val message = if (hour < 12) "Good morning" else "Good afternoon"
// Type is inferred as Int
val value = if (useDefault) 42 else userInput.toInt
// Type is inferred as Option[String]
val maybeValue = if (hasValue) Some("data") else None
// When types don't match exactly, common supertype is used
val mixed = if (condition) 42 else "hello" // Type: Any
val numbers = if (condition) 42 else 3.14 // Type: Double (Int widens to Double)
Practical Examples
Example 1: Grade Calculator
object GradeCalculator extends App {
def calculateGrade(score: Int): String = {
if (score < 0 || score > 100) {
"Invalid score"
} else if (score >= 97) {
"A+"
} else if (score >= 93) {
"A"
} else if (score >= 90) {
"A-"
} else if (score >= 87) {
"B+"
} else if (score >= 83) {
"B"
} else if (score >= 80) {
"B-"
} else if (score >= 77) {
"C+"
} else if (score >= 73) {
"C"
} else if (score >= 70) {
"C-"
} else if (score >= 67) {
"D+"
} else if (score >= 65) {
"D"
} else {
"F"
}
}
val testScores = List(95, 87, 76, 64, 101, -5)
testScores.foreach { score =>
val grade = calculateGrade(score)
println(s"Score: $score → Grade: $grade")
}
}
Example 2: Shipping Calculator
object ShippingCalculator extends App {
def calculateShipping(weight: Double, distance: Int, isPriority: Boolean): Double = {
val baseRate = if (weight <= 1.0) {
5.00
} else if (weight <= 5.0) {
8.00
} else if (weight <= 10.0) {
12.00
} else {
15.00 + (weight - 10.0) * 1.50
}
val distanceMultiplier = if (distance <= 100) {
1.0
} else if (distance <= 500) {
1.5
} else {
2.0
}
val priorityMultiplier = if (isPriority) 1.8 else 1.0
val total = baseRate * distanceMultiplier * priorityMultiplier
// Apply minimum shipping cost
if (total < 3.00) 3.00 else total
}
// Test different scenarios
println(f"Small package, local: $$${calculateShipping(0.5, 50, false)}%.2f")
println(f"Medium package, long distance: $$${calculateShipping(3.0, 800, false)}%.2f")
println(f"Large package, priority: $$${calculateShipping(12.0, 200, true)}%.2f")
}
Example 3: User Access Control
case class User(name: String, role: String, isActive: Boolean, loginCount: Int)
object AccessControl extends App {
def checkAccess(user: User, resource: String): String = {
if (!user.isActive) {
"Access denied: Account is inactive"
} else if (user.loginCount == 0) {
"Access denied: Please change your default password"
} else {
resource.toLowerCase match {
case "admin_panel" =>
if (user.role == "admin") "Access granted"
else "Access denied: Admin privileges required"
case "reports" =>
if (user.role == "admin" || user.role == "manager") "Access granted"
else "Access denied: Management privileges required"
case "profile" =>
"Access granted" // Everyone can access their profile
case _ =>
if (user.role == "admin") "Access granted"
else "Access denied: Unknown resource"
}
}
}
val users = List(
User("Alice", "admin", true, 15),
User("Bob", "user", true, 5),
User("Charlie", "manager", false, 8),
User("Diana", "user", true, 0)
)
val resources = List("admin_panel", "reports", "profile", "secret_data")
for {
user <- users
resource <- resources
} {
val result = checkAccess(user, resource)
println(s"${user.name} accessing $resource: $result")
}
}
Advanced Patterns
Guard Conditions with Complex Logic
def processPayment(amount: Double, accountBalance: Double,
creditLimit: Double, isVipCustomer: Boolean): String = {
val availableFunds = accountBalance + (if (isVipCustomer) creditLimit * 2 else creditLimit)
if (amount <= 0) {
"Invalid amount"
} else if (amount <= accountBalance) {
"Payment processed from account balance"
} else if (amount <= availableFunds) {
val creditUsed = amount - accountBalance
s"Payment processed using $$${creditUsed} credit"
} else if (isVipCustomer && amount <= availableFunds * 1.1) {
"Payment processed with VIP overdraft protection"
} else {
s"Payment declined: Insufficient funds (need $$${amount}, have $$${availableFunds})"
}
}
Early Returns vs Expression Style
// Imperative style (not idiomatic Scala)
def validateUserImperative(email: String, password: String): String = {
if (email.isEmpty) return "Email is required"
if (!email.contains("@")) return "Invalid email format"
if (password.length < 8) return "Password too short"
if (!password.exists(_.isDigit)) return "Password must contain a digit"
"Valid user data"
}
// Expression style (idiomatic Scala)
def validateUser(email: String, password: String): String = {
if (email.isEmpty) {
"Email is required"
} else if (!email.contains("@")) {
"Invalid email format"
} else if (password.length < 8) {
"Password too short"
} else if (!password.exists(_.isDigit)) {
"Password must contain a digit"
} else {
"Valid user data"
}
}
Common Pitfalls and Best Practices
1. Avoid Deep Nesting
// Avoid: Deep nesting makes code hard to read
def processOrder(order: Order): String = {
if (order != null) {
if (order.items.nonEmpty) {
if (order.customer.isActive) {
if (order.total > 0) {
"Order processed"
} else "Invalid total"
} else "Customer inactive"
} else "No items"
} else "Order is null"
}
// Better: Use early returns or guard clauses
def processOrderBetter(order: Order): String = {
if (order == null) return "Order is null"
if (order.items.isEmpty) return "No items"
if (!order.customer.isActive) return "Customer inactive"
if (order.total <= 0) return "Invalid total"
"Order processed"
}
// Best: Use Option/Either for null safety (covered in later lessons)
def processOrderBest(order: Option[Order]): String = {
order match {
case None => "Order is null"
case Some(o) if o.items.isEmpty => "No items"
case Some(o) if !o.customer.isActive => "Customer inactive"
case Some(o) if o.total <= 0 => "Invalid total"
case Some(_) => "Order processed"
}
}
2. Consider Type Compatibility
// Be aware of type widening
val result = if (condition) 42 else 3.14 // Type: Double
// Sometimes you want to be explicit
val result2: Any = if (condition) "hello" else 42 // Explicit Any type
// Or use a common supertype
sealed trait Result
case class Success(value: Int) extends Result
case class Error(message: String) extends Result
val result3: Result = if (condition) Success(42) else Error("failed")
3. Use Meaningful Variable Names
// Poor: Unclear conditions
val result = if (x > 5 && y < 10 && z) a else b
// Better: Extract to meaningful variables
val isValidSize = x > 5
val isWithinLimit = y < 10
val isEnabled = z
val shouldUseOptionA = isValidSize && isWithinLimit && isEnabled
val result = if (shouldUseOptionA) optionA else optionB
When to Use if/else vs Other Constructs
Use if/else when:
- Simple conditional logic with 2-3 branches
- Boolean conditions are straightforward
- You're working with simple values
Consider alternatives when:
- Many conditions (use pattern matching)
- Complex data structure inspection (use pattern matching)
- Working with optional values (use Option and map/flatMap)
- Error handling (use Either or Try)
// Good for if/else
val discount = if (isPremiumMember) 0.1 else 0.05
// Better with pattern matching (covered in later lessons)
val greeting = dayOfWeek match {
case "Monday" => "Start of the work week!"
case "Friday" => "TGIF!"
case "Saturday" | "Sunday" => "Weekend!"
case _ => "Regular day"
}
// Better with Option (covered in later lessons)
val userName = userOption.map(_.name).getOrElse("Guest")
Summary
In this lesson, you've learned about Scala's expression-oriented approach to conditional logic:
✅ Everything is an Expression: if/else
returns values, enabling more concise code
✅ Type Inference: Scala automatically determines the result type
✅ Boolean Logic: AND (&&
), OR (||
), NOT (!
) with short-circuit evaluation
✅ Nested Conditions: Complex decision trees with clear structure
✅ Best Practices: Avoiding deep nesting, meaningful names, type awareness
✅ Function Integration: Using conditionals naturally in function bodies
The expression-oriented nature of Scala is a fundamental concept that you'll use constantly. It leads to more functional, composable code and reduces the need for temporary variables.
What's Next
In the next lesson, we'll explore "Writing Reusable Code with Functions." You'll learn how to define functions using def
, specify parameter and return types, and understand how Scala's powerful type inference works with function definitions.
We'll see how functions are first-class citizens in Scala and how this enables powerful functional programming patterns that make your code more modular and reusable.
Ready to write your first functions? Let's continue!
Comments
Be the first to comment on this lesson!