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!