Scala - If/Else Expressions
Unlike Java or C++ where if/else are statements, Scala treats them as expressions that evaluate to a value. This fundamental difference enables assigning the result directly to a variable without...
Key Insights
- Scala’s if/else constructs are expressions that return values, not statements, enabling functional programming patterns and eliminating the need for ternary operators
- Every if/else expression has a type determined by the unified type of all branches, with Unit as the fallback when branches have incompatible types
- Pattern matching and guard clauses provide more powerful alternatives to nested if/else chains for complex conditional logic
If/Else as Expressions
Unlike Java or C++ where if/else are statements, Scala treats them as expressions that evaluate to a value. This fundamental difference enables assigning the result directly to a variable without temporary mutations.
val age = 25
val category = if (age >= 18) "adult" else "minor"
// category: String = "adult"
// Compare with statement-based approach (anti-pattern in Scala)
var categoryBad = ""
if (age >= 18) {
categoryBad = "adult"
} else {
categoryBad = "minor"
}
The expression-based approach eliminates mutable variables and makes code more concise. The compiler infers the result type from both branches, requiring them to share a common type.
Type Inference and Unified Types
When branches return different types, Scala computes the least upper bound (LUB) to determine the expression’s type.
val result = if (true) 42 else "hello"
// result: Any = 42
val numericResult = if (true) 42 else 3.14
// numericResult: Double = 42.0
val optionResult = if (false) Some(10) else None
// optionResult: Option[Int] = None
In the first example, Int and String have Any as their common supertype. In the second, Int is widened to Double. The third example shows how Some[Int] and None.type unify to Option[Int].
Omitting the Else Branch
When you omit the else branch, the expression returns Unit for the false case, which typically indicates you’re using if for side effects rather than value computation.
val result = if (age >= 18) "adult"
// result: Any = "adult"
// Equivalent to
val explicitResult = if (age >= 18) "adult" else ()
// explicitResult: Any = "adult"
The type becomes Any because it’s the LUB of String and Unit. This pattern usually signals imperative code:
if (temperature > 100) {
println("Warning: High temperature!")
}
// Returns Unit, used for side effect only
Multi-Line Blocks
If/else expressions support multi-line blocks. The last expression in each block becomes the branch’s value.
val discount = if (isPremiumMember) {
val baseDiscount = 0.15
val loyaltyBonus = 0.05
baseDiscount + loyaltyBonus
} else {
0.10
}
// discount: Double = 0.20 (if isPremiumMember is true)
Avoid mixing side effects with value returns in the same block. Keep expression blocks pure when possible:
// Anti-pattern: mixing side effects with return value
val result = if (condition) {
println("Debug message") // side effect
calculateValue() // return value
} else {
42
}
// Better: separate concerns
if (condition) println("Debug message")
val result = if (condition) calculateValue() else 42
Chaining If/Else Expressions
Chain multiple conditions using else-if patterns. Scala evaluates them top-to-bottom, short-circuiting on the first match.
def getGrade(score: Int): String = {
if (score >= 90) "A"
else if (score >= 80) "B"
else if (score >= 70) "C"
else if (score >= 60) "D"
else "F"
}
val grade = getGrade(85) // "B"
For complex conditions, pattern matching provides better readability:
def getGradeWithMatch(score: Int): String = score match {
case s if s >= 90 => "A"
case s if s >= 80 => "B"
case s if s >= 70 => "C"
case s if s >= 60 => "D"
case _ => "F"
}
Nested If/Else Expressions
Nesting if/else expressions is valid but can harm readability. Consider refactoring deeply nested conditions.
// Nested approach (harder to read)
def calculateShipping(weight: Double, isPremium: Boolean, isInternational: Boolean): Double = {
if (isPremium) {
if (isInternational) {
if (weight > 10) 50.0 else 30.0
} else {
0.0 // free shipping
}
} else {
if (isInternational) {
if (weight > 10) 80.0 else 50.0
} else {
if (weight > 10) 15.0 else 10.0
}
}
}
// Flattened with early returns (better)
def calculateShippingFlat(weight: Double, isPremium: Boolean, isInternational: Boolean): Double = {
if (isPremium && !isInternational) return 0.0
val baseRate = (isPremium, isInternational, weight > 10) match {
case (true, true, true) => 50.0
case (true, true, false) => 30.0
case (false, true, true) => 80.0
case (false, true, false) => 50.0
case (false, false, true) => 15.0
case (false, false, false) => 10.0
}
baseRate
}
Using If/Else with Options
Combine if/else with Option types for null-safe code. However, prefer Option’s built-in methods when possible.
val maybeUser: Option[User] = findUser(userId)
// Using if/else
val userName = if (maybeUser.isDefined) maybeUser.get.name else "Anonymous"
// Better: use map and getOrElse
val userNameBetter = maybeUser.map(_.name).getOrElse("Anonymous")
// Or fold for more control
val greeting = maybeUser.fold("Hello, Guest") { user =>
s"Hello, ${user.name}"
}
Guard Clauses and Early Returns
Use guard clauses at function boundaries to handle invalid inputs early, reducing nesting.
def processOrder(order: Order): Either[String, Receipt] = {
if (order.items.isEmpty)
return Left("Order cannot be empty")
if (order.total < 0)
return Left("Invalid order total")
if (!order.customer.isVerified)
return Left("Customer not verified")
// Main logic without nesting
val receipt = generateReceipt(order)
Right(receipt)
}
While early returns work in Scala, functional style prefers composing validations:
def processOrderFunctional(order: Order): Either[String, Receipt] = {
for {
_ <- Either.cond(order.items.nonEmpty, (), "Order cannot be empty")
_ <- Either.cond(order.total >= 0, (), "Invalid order total")
_ <- Either.cond(order.customer.isVerified, (), "Customer not verified")
receipt <- Right(generateReceipt(order))
} yield receipt
}
Performance Considerations
If/else expressions compile to bytecode similar to Java’s conditional statements. The JVM optimizes them effectively, including branch prediction.
// These have equivalent performance
val result1 = if (x > 0) "positive" else "non-positive"
val result2 = if (x > 0) { "positive" } else { "non-positive" }
// Pattern matching may have slight overhead for simple conditions
val result3 = x match {
case n if n > 0 => "positive"
case _ => "non-positive"
}
For hot paths with simple conditions, if/else is optimal. Use pattern matching for clarity when handling complex data structures, not for micro-optimization.
When to Use Alternatives
Replace if/else chains when:
- Matching on case classes or sealed traits: use pattern matching
- Handling multiple boolean flags: use pattern matching on tuples
- Dealing with Option/Either/Try: use map, flatMap, fold, or for-comprehensions
- Implementing polymorphic behavior: use type classes or subtype polymorphism
// Poor: if/else on type checks
if (animal.isInstanceOf[Dog]) {
animal.asInstanceOf[Dog].bark()
} else if (animal.isInstanceOf[Cat]) {
animal.asInstanceOf[Cat].meow()
}
// Better: pattern matching
animal match {
case dog: Dog => dog.bark()
case cat: Cat => cat.meow()
case _ => ()
}
// Best: polymorphism
animal.makeSound()
Scala’s if/else expressions provide a clean, functional approach to conditional logic. Master them alongside pattern matching and Option handling to write expressive, type-safe code.