옵션 없는 패턴 매칭
옵션 없는 패턴 매칭
스칼라 3의 패턴 매칭 구현은 스칼라 2에 비해 크게 단순해졌어요. 사용자 관점에서 보면 스칼라 3이 만들어내는 패턴은 디버그하기 훨씬 쉬운데, 변수들이 디버그 모드에서 전부 보이고 위치도 정확하게 보존되기 때문이에요. 또 스칼라 3은 스칼라 2의 추출자(extractor)를 모두 포함하는 상위 집합을 지원해요.
본문
추출자 (Extractors)
추출자는 unapply나 unapplySeq 메서드를 노출하는 객체예요.
def unapply(x: T): U
def unapplySeq(x: T): U
여기서 T는 임의의 타입인데, 만약 스크루티니(scrutinee) 타입 Scrut의 하위 타입이라면 메서드를 호출하기 전에 타입 검사가 이뤄져요. U는 아래에서 설명하는 고정 항 추출자(Fixed Arity Extractors)와 가변 항 추출자(Variadic Extractors)에서 설명하는 규칙을 따르게 돼요.
참고로 U는 추출자 객체 자신의 타입이 될 수도 있어요.
unapply와 unapplySeq는 사실 더 일반적인 시그니처를 가질 수 있는데, 앞에 타입 절(type clause)이 붙고, 정규 항 절 앞뒤로 using 절이 몇 개든 올 수 있으며, 마지막에는 implicit 절 하나가 최대 하나까지 올 수 있어요. 예를 들면 이런 형태예요.
def unapply[A, B](using C)(using D)(x: T)(using E)(using F)(implicit y: G): U = ???
unapply 메서드를 노출하는 추출자를 고정 항 추출자라고 부르고, 고정된 항 개수의 패턴과 함께 동작해요. unapplySeq 메서드를 노출하는 추출자를 가변 항 추출자라고 부르고, 가변적인 항 개수의 패턴을 지원해요.
고정 항 추출자 (Fixed-Arity Extractors)
고정 항 추출자는 다음 시그니처를 노출해요 (타입 절과 using, implicit 절이 붙을 수 있어요).
def unapply(x: T): U
타입 U는 다음 매칭 중 하나를 따르거나,
- 불리언 매칭 (Boolean match)
- 프로덕트 매칭 (Product match)
아니면 U가 다음 타입 R에 부합할 수 있어요.
type R = {
def isEmpty: Boolean
def get: S
}
그리고 S는 다음 매칭 중 하나를 따르게 돼요.
- 단일 매칭 (single match)
- 이름 기반 매칭 (name-based match)
앞쪽 형태의 unapply가 더 높은 우선순위를 갖고, 단일 매칭이 이름 기반 매칭보다 더 높은 우선순위를 가져요. 참고로 R 안의 S는 U일 수도 있어요.
고정 항 추출자의 사용은 다음 조건 중 하나가 성립하면 불가반(irrefutable) 이 돼요. 즉 패턴이 항상 성공한다는 뜻이에요.
U = true- 추출자가 프로덕트 매칭으로 사용되는 경우
U <: R이고U <: { def isEmpty: false }U = Some[T]
마지막 규칙이 꼭 필요한 이유는, 호환성을 위해 Some의 isEmpty가 항상 false를 돌려줌에도 반환 타입이 false가 아니라 Boolean이기 때문이에요.
불리언 매칭 (Boolean Match)
U =:= Boolean
정확히 0개의 패턴으로 패턴 매칭을 하는 경우예요. 예를 들면 이런 코드죠.
object Even:
def unapply(s: String): Boolean = s.size % 2 == 0
"even" match
case s @ Even() => println(s"$s has an even number of characters")
case s => println(s"$s has an odd number of characters")
// even has an even number of characters
프로덕트 매칭 (Product Match)
U <: ProductN > 0은U에서 연속으로 나타나는(val 또는 파라미터 없는 def) _1: P1 ... _N: PN멤버의 최대 개수예요.- 타입이
P1, P2, ..., PN인 정확히N개의 패턴으로 패턴 매칭을 하는 경우예요.
예를 들면 이런 코드예요.
class FirstChars(s: String) extends Product:
def _1 = s.charAt(0)
def _2 = s.charAt(1)
// Not used by pattern matching: Product is only used as a marker trait.
def canEqual(that: Any): Boolean = ???
def productArity: Int = ???
def productElement(n: Int): Any = ???
object FirstChars:
def unapply(s: String): FirstChars = new FirstChars(s)
"Hi!" match
case FirstChars(char1, char2) =>
println(s"First: $char1; Second: $char2")
// First: H; Second: i
단일 매칭 (Single Match)
타입이 S인 1개의 패턴으로 패턴 매칭을 하는 경우예요. Nat <: R, S = Int인 예시를 볼게요.
class Nat(val x: Int):
def get: Int = x
def isEmpty = x < 0
object Nat:
def unapply(x: Int): Nat = new Nat(x)
5 match
case Nat(n) => println(s"$n is a natural number")
case _ => ()
// 5 is a natural number
이름 기반 매칭 (Name-based Match)
S에는N > 1개의 멤버가 있어서 각각이val또는 파라미터 없는def이고,_1(타입P1)부터_N(타입PN)까지 이름이 붙어 있어요.S에는 위 조건을 만족하는N+1개의 멤버가 없어요. 즉N이 최댓값이에요.- 타입이
P1, P2, ..., PN인 정확히N개의 패턴으로 패턴 매칭을 하는 경우예요.
U = AlwaysEmpty.type <: R, S = NameBased인 예시예요.
object MyPatternMatcher:
def unapply(s: String) = AlwaysEmpty
object AlwaysEmpty:
def isEmpty = true
def get = NameBased
object NameBased:
def _1: Int = ???
def _2: String = ???
"" match
case MyPatternMatcher(_, _) => ???
case _ => ()
가변 항 추출자 (Variadic Extractors)
가변 항 추출자는 다음 시그니처를 노출해요 (타입 절과 using, implicit 절이 붙을 수 있어요).
def unapplySeq(x: T): U
여기서 U는 다음 조건을 만족해야 해요.
V := U로 둡니다.V가 다음 매칭 중 하나에 부합하면 유효해요.- 시퀀스 매칭 (sequence match)
- 프로덕트-시퀀스 매칭 (product-sequence match)
- 그렇지 않으면
U는 다음 타입R에 부합해야 해요.
type R = {
def isEmpty: Boolean
def get: S
}
V := S로 두고 2번을 다시 시도하고, 그래도 실패하면 U는 유효하지 않아요.
V := U 형태의 unapplySeq가 더 높은 우선순위를 갖고, 시퀀스 매칭이 프로덕트-시퀀스 매칭보다 높은 우선순위를 가져요. 참고로 V := U 형태가 유효하면 isEmpty는 무시된다는 뜻이에요.
가변 항 추출자의 사용은 다음 조건 중 하나가 성립하면 불가반이 돼요.
- 추출자가 시퀀스 매칭 또는 프로덕트-시퀀스 매칭으로 직접 사용되는 경우
U <: R이고U <: { def isEmpty: false }U = Some[T]
마지막 규칙이 필요한 이유는 위 고정 항 추출자와 마찬가지로, 호환성을 위해 Some의 isEmpty가 반환 타입이 Boolean이기 때문이에요.
주의할 점이 하나 있어요. 첫 번째 조건과 위 참고 사항 때문에 def isEmpty: true를 갖는 불가반 추출자를 정의하는 게 가능해요. 이런 정의는 강하게 권장되지 않고, 실제 코드에서 발견된다면 거의 확실히 버그예요.
시퀀스 매칭 (Sequence Match)
V <: X
type X = {
def lengthCompare(len: Int): Int // or, `def length: Int`
def apply(i: Int): T1
def drop(n: Int): scala.Seq[T2]
def toSeq: scala.Seq[T3]
}
T2와T3는T1에 부합해요.N이 시퀀스의 런타임 크기일 때, 타입이T1, T1, ..., T1인 정확히N개의 단순 패턴으로 패턴 매칭을 하거나,N이 시퀀스의 최소 크기일 때, 타입이T1, T1, ..., T1, Seq[T1]인>= N개의 단순 패턴과 가변 항 패턴(예:xs: _*)으로 패턴 매칭을 해요.
V = S, U = Option[S] <: R, S = Seq[Char]인 예시예요.
object CharList:
def unapplySeq(s: String): Option[Seq[Char]] = Some(s.toList)
"example" match
case CharList(c1, c2, c3, c4, _, _, _) =>
println(s"$c1,$c2,$c3,$c4")
case _ =>
println("Expected *exactly* 7 characters!")
// e,x,a,m
프로덕트-시퀀스 매칭 (Product-Sequence Match)
V <: ProductN > 0은V에서 연속으로 나타나는(val 또는 파라미터 없는 def) _1: P1 ... _N: PN멤버의 최대 개수예요.PN은 시퀀스 패턴(Seq Pattern)에서 정의한 시그니처X에 부합해요.- 정확히
>= N개의 패턴으로 패턴 매칭을 하고, 처음N - 1개의 패턴은 타입이P1, P2, ... P(N-1)이고, 나머지 패턴의 타입은 시퀀스 패턴에서 정한 대로 결정돼요.
V = S, U = Option[S] <: R, S = (String, PN) <: Product, PN = Seq[Int]인 예시예요.
class Foo(val name: String, val children: Int*)
object Foo:
def unapplySeq(f: Foo): Option[(String, Seq[Int])] =
Some((f.name, f.children))
def foo(f: Foo) = f match
case Foo(name, x, y, ns*) => ">= two children."
case Foo(name, ns*) => "< two children."
앞으로 더 단순화할 계획이 있는데, 특히 프로덕트 매칭과 이름 기반 매칭을 하나의 추출자 타입으로 합치는 작업이 있어요.
타입 검사 (Type testing)
ClassTag를 이용한 추상 타입 검사는 TypeTest 또는 그 별칭 Typeable로 대체됐어요.
- 추상 타입에 대한 패턴
_: X는 스코프 안에TypeTest가 필요해요. - 추상 타입을 받는
unapply에 대한 패턴x @ X()는 스코프 안에TypeTest가 필요해요.
TypeTest에 대한 자세한 내용은 별도 문서를 참고하세요.