매크로로 타입 클래스 derived 메서드 작성하기
매크로로 타입 클래스 derived 메서드 작성하기 (How to write a type class derived method using macros)
주요 파생 문서 페이지에서 Mirror과 타입 클래스 파생 뒤의 세부사항을 설명했어요. 여기서는 매크로만으로 타입 클래스 derived 메서드를 구현하는 방법을 보여드릴게요. Eq 인스턴스를 파생하는 같은 예시를 따르면서, 단순함을 위해 프로덕트 타입(예: case 클래스 Person)만 지원해요. derived 메서드를 구현하는 데 쓸 저수준 기법은 quote와 표현식 및 타입의 splice, 그리고 scala.compiletime.summonFrom과 동등한 scala.quoted.Expr.summon 메서드를 활용해요. 전자는 quote 컨텍스트 안에서 쓰기에 적합하고, 매크로 안에서 사용돼요.
원본 코드와 마찬가지로 타입 클래스 정의는 같아요.
trait Eq[T]:
def eqv(x: T, y: T): Boolean
Eq의 컴패니언 객체에 Eq[T]에 대한 quote된 인스턴스를 만들기 위해 매크로를 호출하는 inline 메서드 Eq.derived를 구현해야 해요. 가능한 시그니처는 다음과 같아요.
inline def derived[T]: Eq[T] = ${ derivedMacro[T] }
def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] = ???
참고로 이후 매크로 컴파일 단계에서 타입이 쓰이므로, 해당 컨텍스트 바운드(derivedMacro에서 볼 수 있음)를 써서 quote된 Type으로 끌어올려야 해요.
비교를 위해, 주요 파생 페이지의 inline derived 메서드의 시그니처는 다음과 같아요.
inline def derived[T](using m: Mirror.Of[T]): Eq[T] = ???
매크로 기반 derived 시그니처에는 Mirror 파라미터가 없다는 점을 주목하세요. derivedMacro의 본문 안에서 Mirror을 summon할 수 있기 때문에 시그니처에서 생략할 수 있어요.
inline이 있는 것과 대비해 derivedMacro 본문에서 얻는 한 가지 추가 가능성은, 매크로로 eqv의 완전히 최적화된 메서드 본문을 만들기가 더 간단하다는 점이에요.
다음 case 클래스 Person에 대한 Eq 인스턴스를 파생하고 싶다고 해볼게요.
case class Person(name: String, age: Int) derives Eq
우리가 생성할 등가성 검사는 다음과 같아요.
(x: Person, y: Person) =>
summon[Eq[String]].eqv(x.productElement(0), y.productElement(0))
&& summon[Eq[Int]].eqv(x.productElement(1), y.productElement(1))
참고로 리플렉션 API를 쓰면 더 최적화해서 Person의 필드를 직접 참조하는 것도 가능하지만, 명확한 이해를 위해 quote된 표현식만 쓰겠어요.
이 본문을 생성하는 코드는 eqProductBody 메서드에서 볼 수 있는데, derivedMacro 메서드 정의의 일부로 여기 보여드릴게요.
def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] =
val ev: Expr[Mirror.Of[T]] = Expr.summon[Mirror.Of[T]].get
ev match
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} =>
val elemInstances = summonInstances[T, elementTypes]
def eqProductBody(x: Expr[Product], y: Expr[Product])(using Quotes): Expr[Boolean] = {
if elemInstances.isEmpty then
Expr(true)
else
elemInstances.zipWithIndex.map {
case ('{ $elem: Eq[t] }, index) =>
val indexExpr = Expr(index)
val e1 = '{ $x.productElement($indexExpr).asInstanceOf[t] }
val e2 = '{ $y.productElement($indexExpr).asInstanceOf[t] }
'{ $elem.eqv($e1, $e2) }
}.reduce((acc, elem) => '{ $acc && $elem })
end if
}
'{ eqProduct((x: T, y: T) => ${eqProductBody('x.asExprOf[Product], 'y.asExprOf[Product])}) }
// case for Mirror.SumOf[T] ...
참고로 매크로가 없는 버전에서는 inline 메서드 안에 그냥 summonInstances[T, m.MirroredElemTypes]라고 쓸 수 있지만, 여기서는 Expr.summon이 필요하므로 요소 타입을 매크로 방식으로 추출할 수 있어요. 매크로 안에 있으니 우리의 첫 반응은 아래 코드를 쓰는 것일 거예요.
'{
summonInstances[T, $m.MirroredElemTypes]
}
하지만 타입 인자 안의 경로(path)가 안정적이지 않기 때문에 이건 쓸 수 없어요. 대신 quote에 대한 패턴 매칭, 더 정확히는 리파인된 타입을 써서 요소 타입의 튜플 타입을 추출해요.
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} => ...
아래는 summonInstances를 매크로로 구현한 것으로, 튜플 타입의 각 타입 elem에 대해 deriveOrSummon[T, elem]을 호출해요.
deriveOrSummon을 이해하려면, elem이 부모 타입 T에서 파생되면 재귀 파생(recursive derivation)이라는 점을 생각해요. 재귀 파생은 보통 scala.collection.immutable.:: 같은 타입에서 일어나요. elem이 T에서 파생되지 않으면 컨텍스트에 Eq[elem] 인스턴스가 반드시 존재해야 해요.
def summonInstances[T: Type, Elems: Type](using Quotes): List[Expr[Eq[?]]] =
Type.of[Elems] match
case '[elem *: elems] => deriveOrSummon[T, elem] :: summonInstances[T, elems]
case '[EmptyTuple] => Nil
def deriveOrSummon[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
Type.of[Elem] match
case '[T] => deriveRec[T, Elem]
case _ => '{ summonInline[Eq[Elem]] }
def deriveRec[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
Type.of[T] match
case '[Elem] => '{ error("infinite recursive derivation") }
case _ => derivedMacro[Elem] // recursive derivation
전체 코드는 아래에 있어요.
import compiletime.*
import scala.deriving.*
import scala.quoted.*
trait Eq[T]:
def eqv(x: T, y: T): Boolean
object Eq:
given Eq[String]:
def eqv(x: String, y: String) = x == y
given Eq[Int]:
def eqv(x: Int, y: Int) = x == y
def eqProduct[T](body: (T, T) => Boolean): Eq[T] =
new Eq[T]:
def eqv(x: T, y: T): Boolean = body(x, y)
def eqSum[T](body: (T, T) => Boolean): Eq[T] =
new Eq[T]:
def eqv(x: T, y: T): Boolean = body(x, y)
def summonInstances[T: Type, Elems: Type](using Quotes): List[Expr[Eq[?]]] =
Type.of[Elems] match
case '[elem *: elems] => deriveOrSummon[T, elem] :: summonInstances[T, elems]
case '[EmptyTuple] => Nil
def deriveOrSummon[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
Type.of[Elem] match
case '[T] => deriveRec[T, Elem]
case _ => '{ summonInline[Eq[Elem]] }
def deriveRec[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
Type.of[T] match
case '[Elem] => '{ error("infinite recursive derivation") }
case _ => derivedMacro[Elem] // recursive derivation
inline def derived[T]: Eq[T] = ${ derivedMacro[T] }
def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] =
val ev: Expr[Mirror.Of[T]] = Expr.summon[Mirror.Of[T]].get
ev match
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} =>
val elemInstances = summonInstances[T, elementTypes]
def eqProductBody(x: Expr[Product], y: Expr[Product])(using Quotes): Expr[Boolean] = {
if elemInstances.isEmpty then
Expr(true)
else
elemInstances.zipWithIndex.map {
case ('{ $elem: Eq[t] }, index) =>
val indexExpr = Expr(index)
val e1 = '{ $x.productElement($indexExpr).asInstanceOf[t] }
val e2 = '{ $y.productElement($indexExpr).asInstanceOf[t] }
'{ $elem.eqv($e1, $e2) }
}.reduce((acc, elem) => '{ $acc && $elem })
end if
}
'{ eqProduct((x: T, y: T) => ${eqProductBody('x.asExprOf[Product], 'y.asExprOf[Product])}) }
case '{ $m: Mirror.SumOf[T] { type MirroredElemTypes = elementTypes }} =>
val elemInstances = summonInstances[T, elementTypes]
val elements = Expr.ofList(elemInstances)
def eqSumBody(x: Expr[T], y: Expr[T])(using Quotes): Expr[Boolean] =
val ordx = '{ $m.ordinal($x) }
val ordy = '{ $m.ordinal($y) }
'{ $ordx == $ordy && $elements($ordx).asInstanceOf[Eq[Any]].eqv($x, $y) }
'{ eqSum((x: T, y: T) => ${eqSumBody('x, 'y)}) }
end derivedMacro
end Eq