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:
- The
when
expression - Smart casts
- Destructuring declarations
- Extension functions like
is
andas
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
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:
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:
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:
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:
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:
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:
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:
-
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
-
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:
-
Create a sealed class hierarchy for different shapes (Circle, Rectangle, Triangle) and write a function that calculates the area using pattern matching.
-
Implement a simple expression evaluator that uses pattern matching to handle different types of expressions (numbers, addition, subtraction, etc.).
-
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
- Kotlin Official Documentation: When Expression
- Sealed Classes and Interfaces
- Destructuring Declarations
- Arrow Kt Library - A functional companion to Kotlin with many pattern matching utilities
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! :)