패턴 매칭
패턴 매칭 (Pattern Matching)
패턴 매칭은 값을 패턴과 대조해 보는 메커니즘이에요. 매칭이 성공하면 값을 구성 요소로 분해(deconstruct)할 수도 있죠. Java의 switch 문보다 훨씬 강력하고, 일련의 if/else를 대신하는 데도 그대로 쓸 수 있어요. 여기서는 패턴 매칭의 문법부터 케이스 클래스·문자열·타입 매칭까지, 그리고 sealed 타입의 안전장치를 함께 살펴볼게요.
문법 (Syntax)
매치 표현식은 값, match 키워드, 그리고 하나 이상의 case 절로 이루어져요.
// Scala 2
import scala.util.Random
val x: Int = Random.nextInt(10)
x match {
case 0 => "zero"
case 1 => "one"
case 2 => "two"
case _ => "other"
}
// Scala 3
import scala.util.Random
val x: Int = Random.nextInt(10)
x match
case 0 => "zero"
case 1 => "one"
case 2 => "two"
case _ => "other"
위의 val x는 0과 9 사이의 무작위 정수예요. x는 match 연산자의 왼쪽 피연산자가 되고, 오른쪽에는 네 개의 case가 있는 표현식이 와요. 마지막 case _는 그 밖의 다른 모든 Int 값을 잡아내는 "catch all" case죠. 이 case들을 _대안(alternatives)_이라고도 불러요.
매치 표현식은 값을 가져요:
// Scala 2
def matchTest(x: Int): String = x match {
case 1 => "one"
case 2 => "two"
case _ => "other"
}
matchTest(3) // returns other
matchTest(1) // returns one
// Scala 3
def matchTest(x: Int): String = x match
case 1 => "one"
case 2 => "two"
case _ => "other"
matchTest(3) // returns other
matchTest(1) // returns one
이 매치 표현식은 모든 case가 String을 반환하므로 타입이 String이에요. 그래서 함수 matchTest는 String을 반환해요.
케이스 클래스에 매칭하기
케이스 클래스는 패턴 매칭에서 특히 유용해요:
sealed trait Notification
case class Email(sender: String, title: String, body: String) extends Notification
case class SMS(caller: String, message: String) extends Notification
case class VoiceRecording(contactName: String, link: String) extends Notification
Notification은 sealed 트레이트이고, Email·SMS·VoiceRecording이라는 세 개의 구체적인 Notification 타입을 케이스 클래스로 구현했어요. (sealed 트레이트는 선언과 같은 파일에서만 확장할 수 있어요.) 이제 이 케이스 클래스들에 대해 패턴 매칭을 할 수 있어요:
// Scala 2
def showNotification(notification: Notification): String = {
notification match {
case Email(sender, title, _) =>
s"You got an email from $sender with title: $title"
case SMS(number, message) =>
s"You got an SMS from $number! Message: $message"
case VoiceRecording(name, link) =>
s"You received a Voice Recording from $name! Click the link to hear it: $link"
}
}
val someSms = SMS("12345", "Are you there?")
val someVoiceRecording = 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
// Scala 3
def showNotification(notification: Notification): String =
notification match
case Email(sender, title, _) =>
s"You got an email from $sender with title: $title"
case SMS(number, message) =>
s"You got an SMS from $number! Message: $message"
case VoiceRecording(name, link) =>
s"You received a Voice Recording from $name! Click the link to hear it: $link"
val someSms = SMS("12345", "Are you there?")
val someVoiceRecording = 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
함수 showNotification은 추상 타입 Notification을 파라미터로 받아서 그 타입에 대해 매칭해요. 즉 전달된 값이 Email인지 SMS인지 VoiceRecording인지 알아내는 거죠. case Email(sender, title, _)에서는 sender와 title 필드가 반환값에 쓰이고, body 필드는 _로 무시돼요.
문자열에 매칭하기
s-인터폴레이터는 문자열에 변수를 넣어주기도 하고, 패턴 매칭에도 유용해요:
// Scala 2
val input: String = "Alice is 25 years old"
input match {
case s"$name is $age years old" => s"$name's age is $age"
case _ => "No match"
}
// Result: "Alice's age is 25"
// Scala 3
val input: String = "Alice is 25 years old"
input match
case s"$name is $age years old" => s"$name's age is $age"
case _ => "No match"
// Result: "Alice's age is 25"
이 예시에서 name과 age는 패턴을 바탕으로 문자열의 일부를 추출해요. 구조화된 텍스트를 파싱할 때 유용하죠.
문자열 패턴 매칭에는 추출기 객체(extractor object)를 쓸 수도 있어요:
// Scala 2
object Age {
def unapply(s: String): Option[Int] = s.toIntOption
}
val input: String = "Alice is 25 years old"
val (name, age) = input match {
case s"$name is ${Age(age)} years old" => (name, age)
}
// name: String = Alice
// age: Int = 25
// Scala 3
object Age:
def unapply(s: String): Option[Int] = s.toIntOption
val input: String = "Alice is 25 years old"
val (name, age) = input match
case s"$name is ${Age(age)} years old" => (name, age)
// name: String = Alice
// age: Int = 25
패턴 가드 (Pattern guards)
패턴 가드는 case를 더 구체적으로 만들기 위해 쓰는 불리언 표현식이에요. 패턴 뒤에 if <boolean expression>을 붙이면 돼요:
// Scala 2
def showImportantNotification(notification: Notification, importantPeopleInfo: Seq[String]): String = {
notification match {
case Email(sender, _, _) if importantPeopleInfo.contains(sender) =>
"You got an email from special someone!"
case SMS(number, _) if importantPeopleInfo.contains(number) =>
"You got an SMS from special someone!"
case other =>
showNotification(other) // nothing special, delegate to our original showNotification function
}
}
val importantPeopleInfo = Seq("867-5309", "[email protected]")
val someSms = SMS("123-4567", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")
val importantEmail = Email("[email protected]", "Drinks tonight?", "I'm free after 5!")
val importantSms = 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/123
println(showImportantNotification(importantEmail, importantPeopleInfo)) // prints You got an email from special someone!
println(showImportantNotification(importantSms, importantPeopleInfo)) // prints You got an SMS from special someone!
// Scala 3
def showImportantNotification(notification: Notification, importantPeopleInfo: Seq[String]): String =
notification match
case Email(sender, _, _) if importantPeopleInfo.contains(sender) =>
"You got an email from special someone!"
case SMS(number, _) if importantPeopleInfo.contains(number) =>
"You got an SMS from special someone!"
case other =>
showNotification(other) // nothing special, delegate to our original showNotification function
val importantPeopleInfo = Seq("867-5309", "[email protected]")
val someSms = SMS("123-4567", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")
val importantEmail = Email("[email protected]", "Drinks tonight?", "I'm free after 5!")
val importantSms = 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/123
println(showImportantNotification(importantEmail, importantPeopleInfo)) // prints You got an email from special someone!
println(showImportantNotification(importantSms, importantPeopleInfo)) // prints You got an SMS from special someone!
case Email(sender, _, _) if importantPeopleInfo.contains(sender)에서 패턴은 sender가 중요한 사람 목록에 있을 때만 매칭돼요.
타입만으로 매칭하기
타입을 기준으로도 매칭할 수 있어요:
// Scala 2
sealed trait Device
case class Phone(model: String) extends Device {
def screenOff = "Turning screen off"
}
case class Computer(model: String) extends Device {
def screenSaverOn = "Turning screen saver on..."
}
def goIdle(device: Device): String = device match {
case p: Phone => p.screenOff
case c: Computer => c.screenSaverOn
}
// Scala 3
sealed trait Device
case class Phone(model: String) extends Device:
def screenOff = "Turning screen off"
case class Computer(model: String) extends Device:
def screenSaverOn = "Turning screen saver on..."
def goIdle(device: Device): String = device match
case p: Phone => p.screenOff
case c: Computer => c.screenSaverOn
def goIdle은 Device의 타입에 따라 다른 동작을 해요. 이 패턴은 case 안에서 매칭된 객체의 메서드를 호출해야 할 때 유용하죠. 타입의 첫 글자를 case 식별자로 쓰는 게 관례예요(여기서는 p와 c).
매칭된 패턴을 변수에 바인딩하기
타입에 따라 다른 동작을 하면서 동시에 매칭된 패턴에서 필드를 추출하려면 변수 바인딩을 써요:
// Scala 2
def goIdleWithModel(device: Device): String = device match {
case p @ Phone(model) => s"$model: ${p.screenOff}"
case c @ Computer(model) => s"$model: ${c.screenSaverOn}"
}
// Scala 3
def goIdleWithModel(device: Device): String = device match
case p @ Phone(model) => s"$model: ${p.screenOff}"
case c @ Computer(model) => s"$model: ${c.screenSaverOn}"
sealed 타입
위 예시들에서 기본 타입에 sealed 키워드가 붙어 있는 걸 눈치채셨을 거예요. 이건 추가적인 안전을 제공해요. 컴파일러가 기본 타입이 sealed일 때 매치 표현식의 case들이 빠짐없이(exhaustive) 있는지 검사해 주거든요.
예를 들어 위에서 정의한 showNotification에서 한 case, 예를 들어 VoiceRecording을 잊어버리면 컴파일러가 경고를 냅니다:
// Scala 2
def showNotification(notification: Notification): String = {
notification match {
case Email(sender, title, _) =>
s"You got an email from $sender with title: $title"
case SMS(number, message) =>
s"You got an SMS from $number! Message: $message"
}
}
// Scala 3
def showNotification(notification: Notification): String =
notification match
case Email(sender, title, _) =>
s"You got an email from $sender with title: $title"
case SMS(number, message) =>
s"You got an SMS from $number! Message: $message"
이 정의는 다음과 같은 경고를 만들어요:
match may not be exhaustive.
It would fail on pattern case: VoiceRecording(_, _)
컴파일러가 실패할 입력 예시까지 알려주죠!
반대로 말하면, 이 빠짐없음 검사가 동작하려면 기본 타입의 모든 서브타입을 기본 타입과 같은 파일에 정의해야 해요. 그렇지 않으면 컴파일러가 가능한 case를 전부 알 수가 없거든요. 예를 들어 sealed trait Notification을 정의한 파일 밖에서 Notification의 새 타입을 정의하면 컴파일 에러가 나요:
case class Telepathy(message: String) extends Notification
^
Cannot extend sealed trait Notification in a different source file
정리
스칼라의 패턴 매칭은 케이스 클래스로 표현되는 대수적 타입(algebraic types)을 매칭할 때 가장 유용해요. 또 스칼라는 케이스 클래스와 별개로, 추출기 객체의 unapply 메서드를 이용해 패턴을 직접 정의하는 것도 허용해요.
더 알아보기
- 케이스 클래스 (Case Classes) — 패턴 매칭의 주 재료
- 추출기 객체 (Extractor Objects) —
unapply로 패턴을 직접 정의하는 법 - Scala 3 book의 match 표현식