Skip to main content

Kotlin Pattern Matching

Pattern matching is a powerful feature in functional programming languages that allows you to check a value against a pattern and, when a match is found, extract information or execute code. While Kotlin doesn't have dedicated pattern matching syntax like Scala or Haskell, it provides several features that, when combined, allow for effective pattern matching-like behavior.

Introduction to Pattern Matching in Kotlin

Pattern matching lets you write code that's more declarative and often more readable than traditional imperative approaches with multiple if-else statements. In Kotlin, pattern matching is primarily achieved through:

  1. The when expression
  2. Smart casts
  3. Destructuring declarations
  4. Extension functions like is and as

Let's explore each of these techniques and see how they can be combined to implement pattern matching in Kotlin.

Basic Pattern Matching with the when Expression

The when expression in Kotlin is similar to switch in other languages but much more powerful. It can be used with any type of expression, not just constants.

Simple Value Matching

kotlin
fun describeNumber(n: Int): String = when (n) {
0 -> "Zero"
1, 2, 3 -> "Small number"
in 4..9 -> "Single digit number"
10 -> "Ten"
!in 11..99 -> "Not a two-digit number"
else -> "A number"
}

fun main() {
println(describeNumber(0)) // Output: Zero
println(describeNumber(2)) // Output: Small number
println(describeNumber(7)) // Output: Single digit number
println(describeNumber(10)) // Output: Ten
println(describeNumber(100)) // Output: Not a two-digit number
println(describeNumber(42)) // Output: A number
}

Type Matching

One of the most common pattern matching scenarios is checking and casting types:

kotlin
fun processValue(value: Any): String = when (value) {
is String -> "String of length ${value.length}"
is Int -> "Integer: $value"
is List<*> -> "List with ${value.size} elements"
is Pair<*, *> -> "Pair of ${value.first} and ${value.second}"
else -> "Unknown type"
}

fun main() {
println(processValue("hello")) // Output: String of length 5
println(processValue(42)) // Output: Integer: 42
println(processValue(listOf(1, 2, 3))) // Output: List with 3 elements
println(processValue(Pair("key", "value"))) // Output: Pair of key and value
println(processValue(3.14)) // Output: Unknown type
}

Notice how Kotlin's smart casts automatically cast the value to the appropriate type inside the branch, allowing you to access type-specific properties without explicit casting.

Advanced Pattern Matching Techniques

Destructuring in Pattern Matching

Destructuring allows you to extract multiple values from an object in a single statement, making your pattern matching more powerful:

kotlin
data class Person(val name: String, val age: Int, val role: String)

fun processPerson(person: Any) = when {
person is Person && person.age < 18 -> "Minor: ${person.name}"
person is Person && person.role == "Admin" -> "Administrator: ${person.name}"
person is Person -> {
val (name, age, role) = person
"Person $name, $age years old, role: $role"
}
else -> "Not a person"
}

fun main() {
val p1 = Person("Alice", 15, "User")
val p2 = Person("Bob", 35, "Admin")
val p3 = Person("Charlie", 42, "User")

println(processPerson(p1)) // Output: Minor: Alice
println(processPerson(p2)) // Output: Administrator: Bob
println(processPerson(p3)) // Output: Person Charlie, 42 years old, role: User
println(processPerson("Not a person object")) // Output: Not a person
}

Using when Without an Argument

The when expression without an argument lets you use boolean conditions for pattern matching:

kotlin
fun checkNumber(n: Int): String = when {
n == 0 -> "Zero"
n % 2 == 0 -> "Even number"
n > 100 -> "Large number"
n < 0 -> "Negative number"
else -> "Odd positive number"
}

fun main() {
println(checkNumber(0)) // Output: Zero
println(checkNumber(16)) // Output: Even number
println(checkNumber(-5)) // Output: Negative number
println(checkNumber(200)) // Output: Large number
println(checkNumber(7)) // Output: Odd positive number
}

Sealed Classes for Pattern Matching

Sealed classes are particularly useful for pattern matching in Kotlin. They restrict the hierarchy of a class, making it possible for the compiler to verify that all possible cases have been covered:

kotlin
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String, val code: Int) : Result()
object Loading : Result()
object Empty : Result()
}

fun handleResult(result: Result): String = when (result) {
is Result.Success -> "Success: ${result.data}"
is Result.Error -> "Error ${result.code}: ${result.message}"
Result.Loading -> "Loading..."
Result.Empty -> "No data available"
// No 'else' branch needed as all cases are covered
}

fun main() {
val results = listOf(
Result.Success("Data loaded"),
Result.Error("Network failure", 404),
Result.Loading,
Result.Empty
)

results.forEach {
println(handleResult(it))
}
}

Output:

Success: Data loaded
Error 404: Network failure
Loading...
No data available

Implementing Custom Pattern Matching

For more complex pattern matching scenarios, you can create extension functions that mimic the pattern matching behavior of other functional languages:

kotlin
sealed class Option<out T> {
data class Some<out T>(val value: T) : Option<T>()
object None : Option<Nothing>()
}

// Pattern matching extension function
inline fun <T, R> Option<T>.match(
onSome: (T) -> R,
onNone: () -> R
): R = when (this) {
is Option.Some -> onSome(value)
is Option.None -> onNone()
}

fun main() {
val someValue: Option<Int> = Option.Some(42)
val noValue: Option<Int> = Option.None

val result1 = someValue.match(
onSome = { "Got value: $it" },
onNone = { "No value" }
)

val result2 = noValue.match(
onSome = { "Got value: $it" },
onNone = { "No value" }
)

println(result1) // Output: Got value: 42
println(result2) // Output: No value
}

Real-world Application: State Management

Pattern matching is particularly useful for state management in applications. Here's an example of how you might use it to handle UI state in a Kotlin application:

kotlin
sealed class UiState {
object Initial : UiState()
object Loading : UiState()
data class Success(val data: List<String>) : UiState()
data class Error(val message: String) : UiState()
}

class UserListViewModel {
// In a real app, this would be a LiveData or Flow
var state: UiState = UiState.Initial
private set

fun fetchUsers() {
state = UiState.Loading

// Simulate API call
try {
// Pretend this is the result of an API call
val users = listOf("Alice", "Bob", "Charlie")
state = UiState.Success(users)
} catch (e: Exception) {
state = UiState.Error("Failed to load users: ${e.message}")
}
}
}

fun renderUserList(state: UiState) {
val result = when (state) {
UiState.Initial -> "Please click 'Load Users' to begin."
UiState.Loading -> "Loading users..."
is UiState.Success -> {
if (state.data.isEmpty()) {
"No users found."
} else {
"Users: ${state.data.joinToString(", ")}"
}
}
is UiState.Error -> "Error: ${state.message}"
}

println(result)
}

fun main() {
val viewModel = UserListViewModel()

renderUserList(viewModel.state) // Output: Please click 'Load Users' to begin.

viewModel.fetchUsers()
renderUserList(viewModel.state) // Output: Users: Alice, Bob, Charlie
}

Pattern Matching vs. Polymorphism

While pattern matching is powerful, it's important to understand when to use it versus object-oriented polymorphism:

  1. Use pattern matching when:

    • Working with a fixed set of types (sealed classes)
    • The behavior depends on multiple properties or conditions
    • You need to handle different types in a uniform way
  2. Use polymorphism when:

    • The set of types might expand in the future
    • The behavior is inherent to the object itself
    • You want to encapsulate behavior within classes

Summary

Though Kotlin doesn't have a dedicated pattern matching syntax like some functional languages, it provides a rich set of features that let you implement pattern matching effectively:

  • The when expression provides the foundation for pattern matching
  • Smart casts automatically cast values to the appropriate type in branches
  • Destructuring declarations help extract data from objects
  • Sealed classes ensure exhaustive pattern matching
  • Extension functions allow for custom pattern matching behaviors

Pattern matching in Kotlin can lead to more declarative, readable, and maintainable code compared to traditional imperative approaches.

Exercises

To practice pattern matching in Kotlin:

  1. Create a sealed class hierarchy for different shapes (Circle, Rectangle, Triangle) and write a function that calculates the area using pattern matching.

  2. Implement a simple expression evaluator that uses pattern matching to handle different types of expressions (numbers, addition, subtraction, etc.).

  3. Create a custom pattern matching extension function for Result<T, E> that allows you to handle success and error cases in a functional way.

Additional Resources

Happy pattern matching in Kotlin!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)