옵션 없는 패턴 매칭

옵션 없는 패턴 매칭

스칼라 3의 패턴 매칭 구현은 스칼라 2에 비해 크게 단순해졌어요. 사용자 관점에서 보면 스칼라 3이 만들어내는 패턴은 디버그하기 훨씬 쉬운데, 변수들이 디버그 모드에서 전부 보이고 위치도 정확하게 보존되기 때문이에요. 또 스칼라 3은 스칼라 2의 추출자(extractor)를 모두 포함하는 상위 집합을 지원해요.

출처: Scala 3 Reference

본문

추출자 (Extractors)

추출자는 unapplyunapplySeq 메서드를 노출하는 객체예요.

def unapply(x: T): U
def unapplySeq(x: T): U

여기서 T는 임의의 타입인데, 만약 스크루티니(scrutinee) 타입 Scrut의 하위 타입이라면 메서드를 호출하기 전에 타입 검사가 이뤄져요. U는 아래에서 설명하는 고정 항 추출자(Fixed Arity Extractors)와 가변 항 추출자(Variadic Extractors)에서 설명하는 규칙을 따르게 돼요.

참고로 U는 추출자 객체 자신의 타입이 될 수도 있어요.

unapplyunapplySeq는 사실 더 일반적인 시그니처를 가질 수 있는데, 앞에 타입 절(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 안의 SU일 수도 있어요.

고정 항 추출자의 사용은 다음 조건 중 하나가 성립하면 불가반(irrefutable) 이 돼요. 즉 패턴이 항상 성공한다는 뜻이에요.

  • U = true
  • 추출자가 프로덕트 매칭으로 사용되는 경우
  • U <: R 이고 U <: { def isEmpty: false }
  • U = Some[T]

마지막 규칙이 꼭 필요한 이유는, 호환성을 위해 SomeisEmpty가 항상 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 <: Product
  • N > 0U에서 연속으로 나타나는 (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는 다음 조건을 만족해야 해요.

  1. V := U로 둡니다.
  2. V가 다음 매칭 중 하나에 부합하면 유효해요.
    • 시퀀스 매칭 (sequence match)
    • 프로덕트-시퀀스 매칭 (product-sequence match)
  3. 그렇지 않으면 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]

마지막 규칙이 필요한 이유는 위 고정 항 추출자와 마찬가지로, 호환성을 위해 SomeisEmpty가 반환 타입이 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]
}
  • T2T3T1에 부합해요.
  • 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 <: Product
  • N > 0V에서 연속으로 나타나는 (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에 대한 자세한 내용은 별도 문서를 참고하세요.