Pattern matching is a mechanism for checking a value against a pattern. A successful match can also deconstruct a value into its constituent parts. It is a more powerful version of the switch
statement in Java and it can likewise be used in place of a series of if/else statements.
Syntax
A match expression has a value, the match
keyword, and at least one case
clause.
importscala.util.Randomvalx:Int=Random.nextInt(10)xmatch{case0=>"zero"case1=>"one"case2=>"two"case_=>"other"}
importscala.util.Randomvalx:Int=Random.nextInt(10)xmatchcase0=>"zero"case1=>"one"case2=>"two"case_=>"other"
The val x
above is a random integer between 0 and 9. x
becomes the left operand of the match
operator and on the right is an expression with four cases. The last case _
is a “catch all” case for any other possible Int
values. Cases are also called alternatives.
Match expressions have a value.
defmatchTest(x:Int):String=xmatch{case1=>"one"case2=>"two"case_=>"other"}matchTest(3)// returns othermatchTest(1)// returns one
defmatchTest(x:Int):String=xmatchcase1=>"one"case2=>"two"case_=>"other"matchTest(3)// returns othermatchTest(1)// returns one
This match expression has a type String because all of the cases return String. Therefore, the function matchTest
returns a String.
Matching on case classes
Case classes are especially useful for pattern matching.
sealedtraitNotificationcaseclassEmail(sender:String,title:String,body:String)extendsNotificationcaseclassSMS(caller:String,message:String)extendsNotificationcaseclassVoiceRecording(contactName:String,link:String)extendsNotification
Notification
is a sealed trait which has three concrete Notification types implemented with case classes Email
, SMS
, and VoiceRecording
. (A sealed trait can be extended only in the same file as its declaration.) Now we can do pattern matching on these case classes:
defshowNotification(notification:Notification):String={notificationmatch{caseEmail(sender,title,_)=>s"You got an email from $sender with title: $title"caseSMS(number,message)=>s"You got an SMS from $number! Message: $message"caseVoiceRecording(name,link)=>s"You received a Voice Recording from $name! Click the link to hear it: $link"}}valsomeSms=SMS("12345","Are you there?")valsomeVoiceRecording=VoiceRecording("Tom","voicerecording.org/id/123")println(showNotification(someSms))// prints You got an SMS from 12345! Message: Are you there?println(showNotification(someVoiceRecording))// prints You received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123
defshowNotification(notification:Notification):String=notificationmatchcaseEmail(sender,title,_)=>s"You got an email from $sender with title: $title"caseSMS(number,message)=>s"You got an SMS from $number! Message: $message"caseVoiceRecording(name,link)=>s"You received a Voice Recording from $name! Click the link to hear it: $link"valsomeSms=SMS("12345","Are you there?")valsomeVoiceRecording=VoiceRecording("Tom","voicerecording.org/id/123")println(showNotification(someSms))// prints You got an SMS from 12345! Message: Are you there?println(showNotification(someVoiceRecording))// prints You received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123
The function showNotification
takes as a parameter the abstract type Notification
and matches on the type of Notification
(i.e. it figures out whether it’s an Email
, SMS
, or VoiceRecording
). In the case Email(sender, title, _)
the fields sender
and title
are used in the return value but the body
field is ignored with _
.
Matching on string
The s
-interpolator allows embedding variables in strings and is also useful for pattern matching.
valinput:String="Alice is 25 years old"inputmatch{cases"$name is $age years old"=>s"$name's age is $age"case_=>"No match"}// Result: "Alice's age is 25"
valinput:String="Alice is 25 years old"inputmatchcases"$name is $age years old"=>s"$name's age is $age"case_=>"No match"// Result: "Alice's age is 25"
In this example, name and age extract parts of the string based on the pattern. This is helpful for parsing structured text.
We can also use extractor objects for string pattern matching.
objectAge{defunapply(s:String):Option[Int]=s.toIntOption}valinput:String="Alice is 25 years old"val(name,age)=inputmatch{cases"$name is ${Age(age)} years old"=>(name,age)}// name: String = Alice// age: Int = 25
objectAge:defunapply(s:String):Option[Int]=s.toIntOptionvalinput:String="Alice is 25 years old"val(name,age)=inputmatchcases"$name is ${Age(age)} years old"=>(name,age)// name: String = Alice// age: Int = 25
Pattern guards
Pattern guards are boolean expressions which are used to make cases more specific. Just add if <boolean expression>
after the pattern.
defshowImportantNotification(notification:Notification,importantPeopleInfo:Seq[String]):String={notificationmatch{caseEmail(sender,_,_)ifimportantPeopleInfo.contains(sender)=>"You got an email from special someone!"caseSMS(number,_)ifimportantPeopleInfo.contains(number)=>"You got an SMS from special someone!"caseother=>showNotification(other)// nothing special, delegate to our original showNotification function}}valimportantPeopleInfo=Seq("867-5309","[email protected]")valsomeSms=SMS("123-4567","Are you there?")valsomeVoiceRecording=VoiceRecording("Tom","voicerecording.org/id/123")valimportantEmail=Email("[email protected]","Drinks tonight?","I'm free after 5!")valimportantSms=SMS("867-5309","I'm here! Where are you?")println(showImportantNotification(someSms,importantPeopleInfo))// prints You got an SMS from 123-4567! Message: Are you there?println(showImportantNotification(someVoiceRecording,importantPeopleInfo))// prints You received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123println(showImportantNotification(importantEmail,importantPeopleInfo))// prints You got an email from special someone!println(showImportantNotification(importantSms,importantPeopleInfo))// prints You got an SMS from special someone!
defshowImportantNotification(notification:Notification,importantPeopleInfo:Seq[String]):String=notificationmatchcaseEmail(sender,_,_)ifimportantPeopleInfo.contains(sender)=>"You got an email from special someone!"caseSMS(number,_)ifimportantPeopleInfo.contains(number)=>"You got an SMS from special someone!"caseother=>showNotification(other)// nothing special, delegate to our original showNotification functionvalimportantPeopleInfo=Seq("867-5309","[email protected]")valsomeSms=SMS("123-4567","Are you there?")valsomeVoiceRecording=VoiceRecording("Tom","voicerecording.org/id/123")valimportantEmail=Email("[email protected]","Drinks tonight?","I'm free after 5!")valimportantSms=SMS("867-5309","I'm here! Where are you?")println(showImportantNotification(someSms,importantPeopleInfo))// prints You got an SMS from 123-4567! Message: Are you there?println(showImportantNotification(someVoiceRecording,importantPeopleInfo))// prints You received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123println(showImportantNotification(importantEmail,importantPeopleInfo))// prints You got an email from special someone!println(showImportantNotification(importantSms,importantPeopleInfo))// prints You got an SMS from special someone!
In the case Email(sender, _, _) if importantPeopleInfo.contains(sender)
, the pattern is matched only if the sender
is in the list of important people.
Matching on type only
You can match on the type like so:
sealedtraitDevicecaseclassPhone(model:String)extendsDevice{defscreenOff="Turning screen off"}caseclassComputer(model:String)extendsDevice{defscreenSaverOn="Turning screen saver on..."}defgoIdle(device:Device):String=devicematch{casep:Phone=>p.screenOffcasec:Computer=>c.screenSaverOn}
sealedtraitDevicecaseclassPhone(model:String)extendsDevice:defscreenOff="Turning screen off"caseclassComputer(model:String)extendsDevice:defscreenSaverOn="Turning screen saver on..."defgoIdle(device:Device):String=devicematchcasep:Phone=>p.screenOffcasec:Computer=>c.screenSaverOn
def goIdle
has a different behavior depending on the type of Device
. This is useful when the case needs to call a method on the pattern. It is a convention to use the first letter of the type as the case identifier (p
and c
in this case).
Binding matched patterns to variables
You can use variable binding to get type-dependent behavior while simultaneously extracting fields from the matched pattern.
defgoIdleWithModel(device:Device):String=devicematch{casep@Phone(model)=>s"$model: ${p.screenOff}"casec@Computer(model)=>s"$model: ${c.screenSaverOn}"}
defgoIdleWithModel(device:Device):String=devicematchcasep@Phone(model)=>s"$model: ${p.screenOff}"casec@Computer(model)=>s"$model: ${c.screenSaverOn}"
Sealed types
You may have noticed that in the examples above the base types are qualified with the keyword sealed
. This provides extra safety because the compiler checks that the cases
of a match
expression are exhaustive when the base type is sealed
.
For instance, in the method showNotification
defined above, if we forget one case, say, VoiceRecording
, the compiler emits a warning:
defshowNotification(notification:Notification):String={notificationmatch{caseEmail(sender,title,_)=>s"You got an email from $sender with title: $title"caseSMS(number,message)=>s"You got an SMS from $number! Message: $message"}}
defshowNotification(notification:Notification):String=notificationmatchcaseEmail(sender,title,_)=>s"You got an email from $sender with title: $title"caseSMS(number,message)=>s"You got an SMS from $number! Message: $message"
This definition produces the following warning:
match may not be exhaustive. It would fail on pattern case: VoiceRecording(_, _)
The compiler even provides examples of input that would fail!
On the flip side, exhaustivity checking requires you to define all the subtypes of the base type in the same file as the base type (otherwise, the compiler would not know what are all the possible cases). For instance, if you try to define a new type of Notification
outside of the file that defines the sealed trait Notification
, it will produce a compilation error:
case class Telepathy(message: String) extends Notification ^ Cannot extend sealed trait Notification in a different source file
Notes
Scala’s pattern matching statement is most useful for matching on algebraic types expressed via case classes. Scala also allows the definition of patterns independently of case classes, using unapply
methods in extractor objects.
More resources
- More details on match expressions in the Scala Book