타입 클래스 파생
타입 클래스 파생 (Type Class Derivation)
타입 클래스 파생(type class derivation)은 몇 가지 간단한 조건을 만족하는 타입 클래스에 대해 given 인스턴스를 자동으로 생성하는 방법이에요. 여기서 타입 클래스는 연산 대상 타입을 정하는 단일 타입 파라미터를 가진 어떤 트레이트나 클래스(그리고 특수한 경우인 CanEqual)를 말해요. 흔한 예로는 Eq, Ordering, Show가 있어요. 예를 들어 다음 Tree 대수적 데이터 타입(ADT)이 주어졌을 때,
enum Tree[T] derives Eq, Ordering, Show:
case Branch(left: Tree[T], right: Tree[T])
case Leaf(elem: T)
derives 절은 Tree의 컴패니언 객체에 Eq, Ordering, Show 타입 클래스에 대한 다음 given 인스턴스를 생성해요.
given [T: Eq] => Eq[Tree[T]] = Eq.derived
given [T: Ordering] => Ordering[Tree[T]] = Ordering.derived
given [T: Show] => Show[Tree[T]] = Show.derived
Tree를 **파생 타입(deriving type)**이라 부르고, Eq, Ordering, Show 인스턴스를 **파생 인스턴스(derived instances)**라고 불러요.
참고로 derived는 수동으로 쓸 수도 있는데, 정의를 제어할 수 없을 때 유용해요. 예를 들어 Option에 대한 Ordering을 이렇게 구현할 수 있어요.
given [T: Ordering] => Ordering[Option[T]] = Ordering.derived
derives 절을 쓸 수 있다면 파생 멤버를 직접 참조하는 것은 권장되지 않아요.
모든 데이터 타입이 derives 절을 가질 수 있어요. 이 문서는 주로 Mirror 타입 클래스의 given 인스턴스도 가진 데이터 타입을 중심으로 다뤄요.
본문
정확한 메커니즘 (Exact mechanism)
아래에서 타입 인자를 나열할 때 첫 번째 인덱스가 마지막 인덱스보다 큰 값으로 평가되면 실제로 인자가 없는 경우예요. 예: A[T_2, ..., T_1]은 A를 뜻해요.
DerivingType[T_1, ..., T_N] derives TC라는 클래스/트레이트/객체/열거형에 대해, 파생 인스턴스는 DerivingType의 컴패니언 객체(객체라면 DerivingType 자체)에 생성돼요.
파생 인스턴스의 일반적인 "모양"은 다음과 같아요.
given [...] => (...) => TC[ ... DerivingType[...] ... ] = TC.derived
TC.derived는 왼쪽에 있는 기대 타입에 부합하는 표현식이어야 하고, 필요하면 항 및/또는 타입 추론으로 정교화될 수 있어요.
참고로 TC.derived는 일반 접근이므로, TC.derived 정의가 여러 개면 오버로딩 해석(overloading resolution)이 적용돼요.
파생 인스턴스의 정확한 모양은 DerivingType과 TC의 특성에 따라 달라져요. 먼저 TC를 살펴봐요.
TC가 파라미터를 1개 받음: F
따라서 TC는 어떤 F에 대해 TC[F[A_1, ..., A_K]](K == 0이면 TC[F])로 정의돼요. 인자들의 종류(kind)에 따라 두 가지 경우가 더 있어요.
F와 DerivingType의 모든 인자의 종류가 *인 경우
참고: 이 경우 K == 0이에요. 생성된 인스턴스는 다음과 같아요.
given [T_1: TC, ..., T_N: TC] => TC[DerivingType[T_1, ..., T_N]] = TC.derived
이것이 가장 흔한 경우이고, 서론에서 강조한 경우예요. 참고로 [T_i: TC, ...]는 (using TC[T_i], ...)를 도입하며, 자세한 내용은 컨텍스트 바운드 문서를 보세요. 이 덕분에 파생 멤버가 이 증거들을 접근할 수 있어요. 또 N == 0이면 위는 다음을 뜻해요.
given TC[DerivingType] = TC.derived
예를 들어 클래스
case class Point(x: Int, y: Int) derives Ordering
는 다음 인스턴스를 생성해요.
object Point:
...
given Ordering[Point] = Ordering.derived
F와 DerivingType이 오른쪽에서 종류가 일치하는 파라미터를 가진 경우
이 섹션은 F와 DerivingType의 인자를 오른쪽부터 짝지어 같은 종류를 쌍으로 갖도록 하고, F나 DerivingType(또는 둘 다)의 인자를 모두 소진하는 경우를 다뤄요. F는 파라미터가 적어도 하나 있어야 해요.
일반적인 모양은 다음과 같아요.
given [...] => TC[ [...] =>> DerivingType[...] ] = TC.derived
물론 TC와 DerivingType은 올바른 종류의 타입에 적용돼요. 이걸 동작시키기 위해 3가지 경우로 나눠요.
F와DerivingType이 같은 수의 인자를 받는 경우 (N == K):
given TC[DerivingType] = TC.derived
// simplified form of:
given TC[ [A_1, ..., A_K] =>> DerivingType[A_1, ..., A_K] ] = TC.derived
DerivingType이F보다 적은 인자를 받는 경우 (N < K), 타입 람다에서 가장 오른쪽 파라미터만 써요.
given TC[ [A_1, ..., A_K] =>> DerivingType[A_(K-N+1), ..., A_K] ] = TC.derived
// if DerivingType takes no arguments (N == 0), the above simplifies to:
given TC[ [A_1, ..., A_K] =>> DerivingType ] = TC.derived
F가DerivingType보다 적은 인자를 받는 경우 (K < N), 남는 가장 왼쪽 자리를 given의 타입 파라미터로 채워요.
given [T_1, ... T_(N-K)] => TC[[A_1, ..., A_K] =>> DerivingType[T_1, ... T_(N-K), A_1, ..., A_K]] = TC.derived
TC가 CanEqual 타입 클래스인 경우
따라서 DerivingType[T_1, ..., T_N] derives CanEqual이에요. U_1, ..., U_M을 종류가 *인 DerivingType의 파라미터라고 합시다. (이는 T_i들의 부분집합이에요.)
생성된 인스턴스는 다음과 같아요.
given [T_1L, T_1R, ..., T_NL, T_NR] // every parameter of DerivingType twice
(using CanEqual[U_1L, U_1R], ..., CanEqual[U_ML, U_MR]): // only parameters of DerivingType with kind *
CanEqual[DerivingType[T_1L, ..., T_NL], DerivingType[T_1R, ..., T_NR]] = // again, every parameter
CanEqual.derived
T_i의 바운드는 올바르게 처리돼요. 예: T_2 <: T_1은 T_2L <: T_1L이 돼요.
예를 들어 클래스
class MyClass[A, G[_]](a: A, b: G[B]) derives CanEqual
는 다음 given 인스턴스를 생성해요.
object MyClass:
...
given [A_L, A_R, G_L[_], G_R[_]] => CanEqual[A_L, A_R] => CanEqual[MyClass[A_L, G_L], MyClass[A_R, G_R]] = CanEqual.derived
TC가 자동 파생에 유효하지 않은 경우
오류를 내요. 정확한 오류는 위 조건 중 어느 것이 실패했는지에 따라 달라져요. 예를 들어 TC가 파라미터를 2개 이상 받는데 CanEqual이 아니면, 오류는 DerivingType cannot be unified with the type argument of TC예요.
모든 데이터 타입이 derives 절을 가질 수 있어요. 이 문서의 나머지는 주로 Mirror 타입 클래스의 given 인스턴스를 가진 데이터 타입을 중심으로 다뤄요.
Mirror
scala.deriving.Mirror 타입 클래스 인스턴스는 타입의 구성 요소와 라벨링에 대한 정보를 타입 수준에서 제공해요. 또 상위 수준 라이브러리가 포괄적인 파생 지원을 제공할 수 있게 하는 최소한의 값 수준 인프라스트럭처도 제공해요.
Mirror 타입 클래스의 인스턴스는 컴파일러가 다음에 대해 무조건 자동 생성해요.
- 열거형(enum)과 열거형 케이스,
- case 객체(case object).
Mirror 인스턴스는 다음에 대해 조건부로도 생성돼요.
- 생성자가 호출 지점에서 보이는 case 클래스 (컴패니언이 case 객체가 아니면 항상 참),
- 자식 case가 적어도 하나 존재할 때,
- 각 자식 case가 부모 정의에서 도달 가능할 때,
- sealed 트레이트/클래스에 컴패니언이 없으면, 각 자식 case가 미러링되는 타입의 접두사를 통해 호출 지점에서 도달 가능할 때,
- 그리고 컴파일러가 각 자식 case에 대한
Mirror타입 클래스 인스턴스를 생성할 수 있을 때.
scala.deriving.Mirror 타입 클래스 정의는 다음과 같아요.
sealed trait Mirror:
/** the type being mirrored */
type MirroredType
/** the type of the elements of the mirrored type */
type MirroredElemTypes
/** The mirrored *-type */
type MirroredMonoType
/** The name of the type */
type MirroredLabel <: String
/** The names of the elements of the type */
type MirroredElemLabels <: Tuple
object Mirror:
/** The Mirror for a product type */
trait Product extends Mirror:
/** Create a new instance of type `T` with elements
* taken from product `p`.
*/
def fromProduct(p: scala.Product): MirroredMonoType
trait Sum extends Mirror:
/** The ordinal number of the case class of `x`.
* For enums, `ordinal(x) == x.ordinal`
*/
def ordinal(x: MirroredMonoType): Int
end Mirror
프로덕트 타입(즉 case 클래스와 객체, 그리고 열거형 케이스)은 Mirror.Product의 하위 타입인 미러를 가져요. 섬 타입(즉 프로덕트 자식을 가진 sealed 클래스/트레이트, 그리고 열거형)은 Mirror.Sum의 하위 타입인 미러를 가져요.
위 Tree ADT에 대해 다음 Mirror 인스턴스가 컴파일러에 의해 자동 제공돼요.
// Mirror for Tree
new Mirror.Sum:
type MirroredType = Tree
type MirroredElemTypes[T] = (Branch[T], Leaf[T])
type MirroredMonoType = Tree[_]
type MirroredLabel = "Tree"
type MirroredElemLabels = ("Branch", "Leaf")
def ordinal(x: MirroredMonoType): Int = x match
case _: Branch[_] => 0
case _: Leaf[_] => 1
// Mirror for Branch
new Mirror.Product:
type MirroredType = Branch
type MirroredElemTypes[T] = (Tree[T], Tree[T])
type MirroredMonoType = Branch[_]
type MirroredLabel = "Branch"
type MirroredElemLabels = ("left", "right")
def fromProduct(p: Product): MirroredMonoType =
new Branch(...)
// Mirror for Leaf
new Mirror.Product:
type MirroredType = Leaf
type MirroredElemTypes[T] = Tuple1[T]
type MirroredMonoType = Leaf[_]
type MirroredLabel = "Leaf"
type MirroredElemLabels = Tuple1["elem"]
def fromProduct(p: Product): MirroredMonoType =
new Leaf(...)
주어진 타입에 대해 Mirror를 자동 생성할 수 없으면, 그 타입이 지원되는 섬 타입도 프로덕트 타입도 아닌 이유를 설명하는 오류가 나타나요. 예를 들어 A가 sealed가 아닌 트레이트라면,
No given instance of type deriving.Mirror.Of[A] was found for parameter x of method summon in object Predef. Failed to synthesize an instance of type deriving.Mirror.Of[A]:
* trait A is not a generic product because it is not a case class
* trait A is not a generic sum because it is not a sealed trait
Mirror 타입의 다음 속성들을 주목하세요.
- 속성은 항(term)이 아니라 타입으로 인코딩돼요. 이는 사용되지 않으면 런타임 발자국이 없다는 뜻이고, 스칼라 3의 메타프로그래밍 기능과 함께 쓸 컴파일 타임 기능이라는 뜻이기도 해요.
- 미러링되는 타입이 지역 또는 내부 클래스라는 제한은 없어요.
MirroredType과MirroredElemTypes의 종류는 미러가 인스턴스인 데이터 타입의 종류와 일치해요. 이 덕분에 미러가 모든 종류의 ADT를 지원할 수 있어요.- 섬이나 프로덕트를 위한 별개의 표현 타입은 없어요. (즉 스칼라 2 버전의 Shapeless 같은
HList나Coproduct타입이 없어요.) 대신 데이터 타입의 자식 타입 모음은 평범한, 어쩌면 파라미터화된 튜플 타입으로 표현돼요. 스칼라 3의 메타프로그래밍 기능으로 이 튜플 타입들을 있는 그대로 다룰 수 있고, 그 위에 상위 수준 라이브러리를 만들 수 있어요. - 프로덕트와 섬 타입 모두에서
MirroredElemTypes의 요소는 정의 순서로 배열돼요. (즉Branch가 소스 파일에서Leaf보다 먼저 정의되므로Tree에 대한MirroredElemTypes에서Branch[T]가Leaf[T]보다 앞에 와요.) 따라서Mirror.Sum은 이 점에서 스칼라 2에서 ADT에 대한 Shapeless의 제네릭 표현과 다르다. 거기서 생성자는 이름의 알파벳 순서로 정렬되거든요. ordinal과fromProduct메서드는MirroredMonoType으로 정의되는데, 이는MirroredType에서 타입 파라미터를 와일드카드로 만들어 얻은 종류*의 타입이에요.
Mirror로 derived 구현하기 (Implementing derived with Mirror)
앞서 보았듯 타입 클래스 TC[_]에 대한 derived 메서드의 시그니처와 구현은 임의적이지만, 보통 다음 형태일 거라 기대해요.
import scala.deriving.Mirror
inline def derived[T](using Mirror.Of[T]): TC[T] = ...
즉 derived 메서드는 파생 타입 T의 모양을 정의하는 Mirror 타입의 (어떤 하위 타입의) 컨텍스트 파라미터를 받고, 그 모양에 따라 타입 클래스 구현을 계산해요. 이것이 타입 클래스 인스턴스의 파생에 대해 derives 절을 가진 ADT 제공자가 알면 되는 전부예요.
참고로 derived 메서드는 Mirror 컨텍스트 파라미터를 간접적으로 가질 수도 있어요. (예: 차례로 컨텍스트 Mirror 파라미터를 가진 컨텍스트 인자를 갖거나) 아예 가지지 않을 수도 있어요. (예: 스칼라 3 매크로나 런타임 리플렉션 같은 완전히 다른 사용자 제공 메커니즘을 쓸 수도 있어요.) 우리는 (직접이든 간접이든) Mirror 기반 구현이 가장 흔할 거라 예상하고, 이 문서가 그것을 강조해요.
타입 클래스 작성자(author)는 derived 메서드를 구현하기 위해 상위 수준 파생이나 제네릭 프로그래밍 라이브러리를 쓸 가능성이 가장 커요. 위에서 설명한 저수준 기능과 스칼라 3의 일반 메타프로그래밍 기능만을 써서 derived 메서드를 구현하는 예시를 아래에 제공해요. 타입 클래스 작성자가 보통 이런 식으로 derived 메서드를 구현하리라고 기대하지는 않지만, 이 워크스루는 우리가 일반적인 타입 클래스 작성자가 쓸 거라 기대하는 상위 수준 파생 라이브러리 작성자들의 안내서로 삼을 수 있어요. (그런 라이브러리의 완전한 예시는 Shapeless 3을 보세요.)
저수준 메커니즘으로 타입 클래스 derived 메서드 작성하기 (How to write a type class derived method using low-level mechanisms)
이 예시에서 타입 클래스 derived 메서드를 구현하는 데 쓸 저수준 기법은 스칼라 3의 세 가지 새로운 타입 수준 구성을 활용해요. inline 메서드, inline 매치, 그리고 summonInline 또는 summonFrom을 통한 암시적 검색이에요. Eq 타입 클래스 정의가 주어졌을 때,
trait Eq[T]:
def eqv(x: T, y: T): Boolean
Mirror[T]가 주어지면 Eq[T]에 대한 given 인스턴스를 만드는 Eq.derived 메서드를 Eq의 컴패니언 객체에 구현해야 해요. 가능한 구현은 다음과 같아요.
import scala.deriving.Mirror
inline def derived[T](using m: Mirror.Of[T]): Eq[T] =
lazy val elemInstances = summonInstances[T, m.MirroredElemTypes] // (1)
inline m match // (2)
case s: Mirror.SumOf[T] => eqSum(s, elemInstances)
case p: Mirror.ProductOf[T] => eqProduct(p, elemInstances)
derived가 inline def로 정의된다는 점에 주목하세요. 이는 메서드가 모든 호출 지점에서 인라인된다는 뜻이에요. (예: deriving Eq 절을 가진 ADT의 컴패니언 객체에 있는 컴파일러 생성 인스턴스 정의.)
복잡한 코드를 인라인하는 것은 과하게 쓰면 잠재적으로 비싸져요. (컴파일 시간이 느려진다는 뜻이에요.) 그래서 같은 타입에 대해 derived가 호출되는 횟수를 제한하는 데 주의해야 해요. 예를 들어 섬 타입에 대한 인스턴스를 계산할 때, 각 자식 case에 대한 인스턴스를 계산하기 위해 derived를 재귀적으로 호출해야 할 수도 있어요. 그 자식 case는 차례로 부모 섬 타입을 가리키는 필드를 선언하는 프로덕트 타입일 수도 있어요. 이 필드에 대한 인스턴스를 계산할 때는 derived를 재귀적으로 호출하지 말고 컨텍스트에서 summon해야 해요. 보통 찾은 given 인스턴스는 처음에 derived를 호출한 루트 given 인스턴스일 거예요.
derived 본문(1)은 먼저 인스턴스가 파생되는 타입의 모든 자식 타입에 대한 Eq 인스턴스를 구체화해요. 이는 섬 타입의 모든 브랜치이거나 프로덕트 타입의 모든 필드예요. summonInstances의 구현은 inline이고 스칼라 3의 summonInline 구성을 써서 인스턴스를 List로 모아요.
inline def summonInstances[T, Elems <: Tuple]: List[Eq[?]] =
inline erasedValue[Elems] match
case _: (elem *: elems) => deriveOrSummon[T, elem] :: summonInstances[T, elems]
case _: EmptyTuple => Nil
inline def deriveOrSummon[T, Elem]: Eq[Elem] =
inline erasedValue[Elem] match
case _: T => deriveRec[T, Elem]
case _ => summonInline[Eq[Elem]]
inline def deriveRec[T, Elem]: Eq[Elem] =
inline erasedValue[T] match
case _: Elem => error("infinite recursive derivation")
case _ => Eq.derived[Elem](using summonInline[Mirror.Of[Elem]]) // recursive derivation
자식에 대한 인스턴스를 손에 쥐고, derived 메서드는 inline 매치로 섬이나 프로덕트에 대한 인스턴스를 만들 수 있는 메서드로 디스패치해요 (2). derived가 inline이기 때문에 매치는 컴파일 타임에 해석되고, 매칭되는 case의 오른쪽 변만 매치가 밝힌 대로 타입이 정제된 채 생성된 코드에 인라인된다는 점에 주목하세요.
섬 경우인 eqSum에서는 eqv의 인자들의 런타임 ordinal 값을 써서 먼저 두 값이 ADT의 같은 하위 타입인지 확인하고(3), 같으면 보조 메서드 check를 써서 적절한 ADT 하위 타입에 대한 Eq 인스턴스를 바탕으로 등가성을 추가로 검사해요 (4).
import scala.deriving.Mirror
def eqSum[T](s: Mirror.SumOf[T], elems: => List[Eq[?]]): Eq[T] =
new Eq[T]:
def eqv(x: T, y: T): Boolean =
val ordx = s.ordinal(x) // (3)
(s.ordinal(y) == ordx) && check(x, y, elems(ordx)) // (4)
프로덕트 경우인 eqProduct에서는 eqv의 인자들의 런타임 값을 프로덕트로서 데이터 타입 필드들에 대한 Eq 인스턴스로 검사해요 (5).
import scala.deriving.Mirror
def eqProduct[T](p: Mirror.ProductOf[T], elems: => List[Eq[?]]): Eq[T] =
new Eq[T]:
def eqv(x: T, y: T): Boolean =
iterable(x).lazyZip(iterable(y)).lazyZip(elems).forall(check)
eqSum과 eqProduct 둘 다 이름에 의한 파라미터 elems를 갖는데, 전달되는 인자가 lazy elemInstances 값에 대한 참조이기 때문이에요.
이 모든 것을 합치면 다음 완전한 구현이 돼요.
import scala.collection.AbstractIterable
import scala.compiletime.{erasedValue, error, summonInline}
import scala.deriving.*
inline def summonInstances[T, Elems <: Tuple]: List[Eq[?]] =
inline erasedValue[Elems] match
case _: (elem *: elems) => deriveOrSummon[T, elem] :: summonInstances[T, elems]
case _: EmptyTuple => Nil
inline def deriveOrSummon[T, Elem]: Eq[Elem] =
inline erasedValue[Elem] match
case _: T => deriveRec[T, Elem]
case _ => summonInline[Eq[Elem]]
inline def deriveRec[T, Elem]: Eq[Elem] =
inline erasedValue[T] match
case _: Elem => error("infinite recursive derivation")
case _ => Eq.derived[Elem](using summonInline[Mirror.Of[Elem]]) // recursive derivation
trait Eq[T]:
def eqv(x: T, y: T): Boolean
object Eq:
given Eq[Int]:
def eqv(x: Int, y: Int) = x == y
def check(x: Any, y: Any, elem: Eq[?]): Boolean =
elem.asInstanceOf[Eq[Any]].eqv(x, y)
def iterable[T](p: T): Iterable[Any] = new AbstractIterable[Any]:
def iterator: Iterator[Any] = p.asInstanceOf[Product].productIterator
def eqSum[T](s: Mirror.SumOf[T], elems: => List[Eq[?]]): Eq[T] =
new Eq[T]:
def eqv(x: T, y: T): Boolean =
val ordx = s.ordinal(x)
(s.ordinal(y) == ordx) && check(x, y, elems(ordx))
def eqProduct[T](p: Mirror.ProductOf[T], elems: => List[Eq[?]]): Eq[T] =
new Eq[T]:
def eqv(x: T, y: T): Boolean =
iterable(x).lazyZip(iterable(y)).lazyZip(elems).forall(check)
inline def derived[T](using m: Mirror.Of[T]): Eq[T] =
lazy val elemInstances = summonInstances[T, m.MirroredElemTypes]
inline m match
case s: Mirror.SumOf[T] => eqSum(s, elemInstances)
case p: Mirror.ProductOf[T] => eqProduct(p, elemInstances)
end Eq
이것을 간단한 ADT에 대해 이렇게 테스트할 수 있어요.
enum Lst[+T] derives Eq:
case Cns(t: T, ts: Lst[T])
case Nl
extension [T](t: T) def ::(ts: Lst[T]): Lst[T] = Lst.Cns(t, ts)
@main def test(): Unit =
import Lst.*
val eqoi = summon[Eq[Lst[Int]]]
assert(eqoi.eqv(23 :: 47 :: Nl, 23 :: 47 :: Nl))
assert(!eqoi.eqv(23 :: Nl, 7 :: Nl))
assert(!eqoi.eqv(23 :: Nl, Nl))
이 경우 Lst에 대한 파생 Eq 인스턴스의 인라인 확장으로 생성된 코드는, 약간 다듬으면 다음과 같아요.
given derived$Eq[T] => (eqT: Eq[T]) => Eq[Lst[T]] =
eqSum(summon[Mirror.Of[Lst[T]]], {/* cached lazily */
List(
eqProduct(summon[Mirror.Of[Cns[T]]], {/* cached lazily */
List(summon[Eq[T]], summon[Eq[Lst[T]]])
}),
eqProduct(summon[Mirror.Of[Nl.type]], {/* cached lazily */
Nil
})
)
})
elemInstances의 lazy 수정자는 Lst 같은 재귀 타입에 대한 파생 인스턴스에서 무한 재귀를 막는 데 필요해요.
derived 메서드를 정의하는 방식에 대안적인 접근도 있어요. 예를 들어 스칼라 3 매크로를 쓰는 더 공격적인 인라인 변형은 타입 클래스 작성자가 쓰기에는 위 예시보다 더 복잡하지만, Eq 같은 타입 클래스에 대해 모든 추상화 잔재(위의 자식 인스턴스 리스트 같은 것)를 제거하고 프로그래머가 손으로 쓴 것과 구별할 수 없는 코드를 만들어 낼 수 있어요. 세 번째 예시로, Shapeless 같은 상위 수준 라이브러리를 쓰면 타입 클래스 작성자는 동등한 derived 메서드를 다음과 같이 정의할 수 있어요.
given eqSum: [A] => (inst: => K0.CoproductInstances[Eq, A]) => Eq[A]:
def eqv(x: A, y: A): Boolean = inst.fold2(x, y)(false)(
[t] => (eqt: Eq[t], t0: t, t1: t) => eqt.eqv(t0, t1)
)
given eqProduct: [A] => (inst: => K0.ProductInstances[Eq, A]) => Eq[A]:
def eqv(x: A, y: A): Boolean = inst.foldLeft2(x, y)(true: Boolean)(
[t] => (acc: Boolean, eqt: Eq[t], t0: t, t1: t) =>
Complete(!eqt.eqv(t0, t1))(false)(true)
)
inline def derived[A](using gen: K0.Generic[A]): Eq[A] =
gen.derive(eqProduct, eqSum)
여기서 설명한 프레임워크는 이 세 가지 접근을 모두 가능하게 해 주고, 어느 것도 강제하지 않아요.
매크로로 타입 클래스 derived 메서드를 쓰는 방법에 대한 간단한 논의는 "How to write a type class derived method using macros" 문서를 참고하세요.
문법 (Syntax)
Template ::= InheritClauses [TemplateBody]
EnumDef ::= id ClassConstr InheritClauses EnumBody
InheritClauses ::= [‘extends’ ConstrApps] [‘derives’ QualId {‘,’ QualId}]
ConstrApps ::= ConstrApp {‘with’ ConstrApp}
| ConstrApp {‘,’ ConstrApp}
참고로 extends 절과 derives 절을 맞추기 위해, 스칼라 3은 여러 확장 타입을 콤마로 구분하는 것도 허용해요. 그래서 다음이 이제 합법이에요.
class A extends B, C { ... }
이것은 옛 형태와 동등해요.
class A extends B with C { ... }
논의 (Discussion)
이 타입 클래스 파생 프레임워크는 의도적으로 아주 작고 저수준이에요. 컴파일러가 생성하는 Mirror 인스턴스에는 본질적으로 두 가지 인프라스트럭처 조각이 있어요.
- 미러링된 타입의 속성을 인코딩하는 타입 멤버.
- 미러링된 타입의 항을 제네릭하게 다루기 위한 최소한의 값 수준 메커니즘.
Mirror 인프라는 case 클래스를 위한 기존 Product 인프라의 확장으로 볼 수 있어요. 보통 Mirror 타입은 ADT의 컴패니언 객체가 구현하므로, 타입 멤버와 ordinal 또는 fromProduct 메서드는 그 객체의 멤버가 될 거예요. 이 설계 결정(그리고 속성을 값이 아니라 타입으로 인코딩하기로 한 결정)의 주요 동기는, Mirror 인스턴스를 무조건 제공할 수 있을 정도로 기능의 바이트코드와 런타임 발자국을 작게 유지하는 거였어요.
Mirror이 타입 멤버로 정밀하게 속성을 인코딩하는 반면, 값 수준의 ordinal과 fromProduct는 Product의 멤버처럼 다소 약하게 타입되어 있어요. (MirroredMonoType으로 정의되기 때문이에요.) 이는 제네릭 타입 클래스 코드가 타입 탐색과 값 선택이 발을 맞춰 진행되는지 보장해야 하고, 일부 자리에서 캐스트로 이 적합성을 단언해야 한다는 뜻이에요. 제네릭 타입 클래스가 올바르게 쓰이면 이 캐스트는 결코 실패하지 않아요.
앞서 말했듯 컴파일러가 제공하는 메커니즘은 의도적으로 아주 저수준이고, 상위 수준 타입 클래스 파생과 제네릭 프로그래밍 라이브러리가 이것과 스칼라 3의 다른 메타프로그래밍 기능 위에 구축되어, 타입 클래스 작성자와 일반 사용자에게 이 저수준 세부사항을 숨길 거라 기대돼요. Shapeless와 Magnolia 두 스타일의 타입 클래스 파생이 모두 가능하고 (Shapeless 2와 Magnolia의 양면을 결합한 Shapeless 3의 프로토타입이 이 언어 기능과 함께 개발됐어요), 스칼라 3의 새로운 quote/splice 매크로와 인라인 기능이 지원하는 더 공격적인 인라인 스타일도 가능해요.