Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you something that happens to almost every Scala developer early on. You're feeling confident with your case classes, you've built a nice little hierarchy, and you decide to use a match expression to handle your logic. It looks clean, it looks functional, and then—boom—your application crashes in production with a scala.MatchError.

sealed trait UserRole
case object Admin extends UserRole
case object Editor extends UserRole
case object Viewer extends UserRole

def getPermissions(role: UserRole): String = role match {
  case Admin  => "Full Access"
  case Editor => "Can Edit Content"
  // I forgot to handle Viewer!
}

// This works fine
println(getPermissions(Admin)) 

// This crashes the JVM
println(getPermissions(Viewer)) 

The Runtime Crash of Forgotten Cases

The problem here is that pattern matching in Scala is "exhaustive" only if you make it so. In the example above, I told Scala how to handle Admin and Editor, but I left Viewer hanging. Unlike a switch statement in some other languages that might just do nothing if no case matches, Scala's match is an expression—it must return a value. If it reaches the end of the list without a match, it throws a MatchError.

Now, you might think, "I'll just add a default case at the bottom," and while that stops the crash, it often hides bugs. If I add a case _ => "No Access", and later I add a SuperAdmin role to my trait, the compiler won't tell me that I forgot to give SuperAdmin specific permissions. They'll just silently fall into the "No Access" bucket.

Using Sealed Traits for Compiler Safety

The real fix is to combine sealed traits with the compiler's ability to check for exhaustiveness. By marking the trait as sealed, you're telling Scala: "All implementations of this trait are defined in this single file."

Once you do that, if you miss a case in your match expression, the compiler will give you a warning: "match may not be exhaustive. It would fail on the following input: Viewer". I always treat these warnings as errors. If the compiler tells me I've missed a case, I don't ignore it; I fix the logic.

def getPermissions(role: UserRole): String = role match {
  case Admin  => "Full Access"
  case Editor => "Can Edit Content"
  case Viewer => "Read Only" // Now it's safe and complete
}

Extracting Data with Deconstruction

Pattern matching isn't just for checking types; it's for pulling data out of objects without writing a dozen getters. This is where it gets powerful. Instead of checking if a user is an Admin and then calling user.adminLevel, you do it all in one go.

case class User(name: String, role: UserRole)
case class AdminRole(level: Int) extends UserRole
case object ViewerRole extends UserRole

def welcomeMessage(user: User) = user.role match {
  case AdminRole(lvl) if lvl > 5 => s"Welcome, Overlord ${user.name}!"
  case AdminRole(lvl)            => s"Welcome, Admin ${user.name} (Level $lvl)"
  case ViewerRole                => s"Welcome, ${user.name}. Please enjoy your stay."
}

Notice that if lvl > 5 part? That's called a pattern guard. It allows you to add arbitrary logic to a case. If the pattern matches but the guard is false, Scala just moves on to the next case. It's significantly more readable than nesting if/else blocks inside your match arms.

Matching on Sequences and Lists

One last thing I want you to get comfortable with: matching on collections. You can match the structure of a list, which is incredibly useful for recursive algorithms or processing streams of data.

val numbers = List(1, 2, 3)

numbers match {
  case Nil => "The list is empty"
  case head :: Nil => s"Only one element: $head"
  case head :: second :: tail => s"Starts with $head and $second, followed by $tail"
}

The :: (cons) operator is the key here. It splits the list into the first element and the remainder of the list. It's a pattern I use constantly when parsing data or walking through a tree structure.




📋 Practical Task

Implementing a Robust Notification Dispatcher

You are building a notification system that sends alerts via different channels. Some channels require extra metadata (like a phone number or an API key), while others are simple.

Your task:

  1. Create a sealed trait NotificationChannel.
  2. Implement three case classes/objects:
    • Email(address: String, subject: String)
    • SMS(phoneNumber: String)
    • PushNotification(deviceId: String, priority: Int)
  3. Write a function dispatch(channel: NotificationChannel, message: String): String that uses pattern matching to return a formatted string describing the action:
    • For Email, return: "Sending Email to [address] with subject [subject]: [message]"
    • For SMS, return: "Sending SMS to [phoneNumber]: [message]"
    • For PushNotification, use a pattern guard: if priority is greater than 10, return "URGENT Push to [deviceId]: [message]"; otherwise, return "Standard Push to [deviceId]: [message]"
  4. Ensure your match expression is exhaustive so that it would trigger a compiler warning if a new channel were added.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.