Pattern Matching Magic
A Tour for JVM Developers.
I’m still exploring the JVM universe, and Scala has become my buddy. Together, we unlock the power of pattern matching, one of its handy features.
Pattern matching is checking a value against a pattern. The pattern could be a constant, a variable, a constructor of a case class
or even more complex combinations. A successful match can deconstruct a value into its constituent parts. This can be used in place of a series of if/else statements.
Scala uses match
expressions for pattern matching. For example matching a number to a month of the year:
val monthIndex: Int = 7
val month: String = monthIndex match
case 1 => "January"
case 2 => "February"
case 3 => "March"
case 4 => "April"
case 5 => "May"
case 6 => "June"
case 7 => "July"
case 8 => "August"
case 9 => "September"
case 10 => "October"
case 11 => "November"
case 12 => "December"
case _ => "Not A month" // Default case
monthIndex
is an integer representing index of a month of the year. It is the left operand of the match
expression. On the right side, there are all possible cases of months starting January till December. The last case represented by _
is a “catch all” case for any other possible Int
values.
Scala match
expression offers a rich set of pattern matching capabilities: simple constants comparisons as in the example above and can handle sequences, tuples, type checks and constructor structures. My favourite is constructor pattern matching. Leveraging the structure of a case class
and using constructor pattern matching you’d check the type of value as well as extract fields of the object, all in a single match expression.
Check out Destructuring Assignment article to learn more about unpacking values of an object to variables.
Using a Student
example:
sealed trait Student
case class Undergrad(name: String, course: String, studyMode: String) extends Student
case class GradStudent(name: String, course: String) extends Student
Student
is a sealed trait with two concrete implementations: Undergrad
and GradStudent.
Now, you’d match an instance of Student
like this:
def simpleMatching(student: Student): String = {
student match
case Undergrad(name, courseName, studyMode) =>
s"$name is undergrad student pursuing $courseName as a $studyMode student"
case GradStudent(name, courseName) =>
s"$name is a grad student pursuing $courseName"
}
val student = Undergrad("Bilbo", "Scala", "FullTime")
val result = simpleMatching(student)
println(result) // Prints 'Bilbo is a undergrad student pursuing Scala as a FullTime student'
What about when you have a nested data structure?
Consider the following:
sealed trait Course
case class SoftwareEngineering(track: String, name: String)
case class MachineLearning(name: String)
sealed trait Student
case class Undergrad(name: String, course: Course, studyMode: String) extends Student
case class GradStudent(name: String, course: Course) extends Student
A Course
is a sealed trait. It has two implementations: Software Engineering
and Machine Learning
. Every Student
implementation has a Course
property.
You’d like to match all undergrad students taking software engineering in the mobile track and graduate students taking Machine learning.
Deconstructing values
In Scala, when a pattern successfully matches a value, any variables defined within the pattern become bound to the corresponding parts of the matched value. This allows you to access specific components of a data structure without needing property look ups, with getter methods for instance.
Using deconstructing values feature, then you’d to write a match
expression like this:
def complexMatching(student: Student): String = {
student match
case Undergrad(name, SoftwareEngineering("Mobile", courseName), _) =>
s"$name is on Mobile track pursuing $courseName"
case GradStudent(name, MachineLearning(courseName)) =>
s"$name is a Machine Learning student pursuing $courseName"
case _ => "Other Students"
}
Deconstructing values with pattern matching allows you to extract values from complex data structures while writing concise, expressive and safer code.
PS: Kotlin
Kotlin is defintely my favourite language. Won’t it be great to be able implement complex pattern matching using the language?
Kotlin does not support full-blown pattern matching, yet. If you have complex data type involving deconstructing values, you’d use traditional if-else statements or nested when
expressions.
Using the Studnet
example from above:
sealed interface Course
data class SoftwareEngineering(val track: String, val courseName: String): Course
data class MachineLearning(val courseName: String): Course
sealed interface Student
data class UnderGradStudent(val name: String, val course: Course, val studyMode: String) : Student
data class GradStudent(val name: String, val course: Course): Student
To match undergraduate students taking software engineering on the mobile track and graduate students taking Machine learning, you’ll write code like this:
fun filterStudents(student: Student): String{
return when(student){
is UnderGradStudent -> {
val (name, course, _) = student
when(course){
is SoftwareEngineering -> {
if (course.track == "Mobile") {
"$name is in Mobile track pursuing ${course.courseName}"
}else{ "Other students" }
}
else -> { "Other Students" }
}
}
is GradStudent ->{
val(name, course,) = student
when(course){
is MachineLearning -> {
"$name is a Machine learning student pursuing ${course.courseName}"
}
else -> { "Other Students"}
}
}
}
}
This code is less expressive, a little more verbose? It would benefit from destructuring values feature. Then you’d write code like this:
fun filterStudents(student: Student): String{
return when(student){
is UnderGradStudent(name, SoftwareEngineering("Mobile", courseName),_ ) -> { }
is GradStudent(name, MachineLearning(courseName)) ->{ }
else -> { }
}
}
Potentially, pattern matching with complex types is coming to Kotlin. Check this: Support for pattern matching with complex patterns. And one day, you’d match complex data structures while still maintaining concise and expressive code. 🙌🏽
Resources
Fun coding! Or there and back again.