はじめに
scala3のderivesを実装しながら解説していく。
参考資料
derives とは
Scala3から derives
という機能が追加された。
これはHaskellの deriving宣言 というクラスメソッドの実装方法が自明な場合,deriving宣言を用いてinstance宣言を省略する機能をscalaに持ち込んだものかと思う(多分)
haskellのderiving
data Position = Position { x :: Double, y :: Double } deriving (Show, Monoid)
-- ↑ではShowとMonoidのインスタンスのinstance宣言を省略している
scalaのderives
case class Position(x: Double, y: Double) derives Show, Monoid
実装してみる
Scalaの場合は型クラスごとに生成方法を書いてあげる必要がある。
とりあえず型クラスを定義する
trait Monoid[A]:
def empty: A
def combine(x: A, y: A): A
extension (x: A) def |+| (y: A): A = combine(x, y)
object Monoid:
def apply[T](using m: Monoid[T]): Monoid[T] = m
trait Show[A]:
def show(a: A): String
object Show:
def apply[T](using s: Show[T]): Show[T] = s
extension [A](a: A)(using S: Show[A]) def show: String = S.show(a)
// Show内に記述すると同名メソッドでエラーになるので雑に外に出してる
次にADT内のフィールドに対応するインスタンスを定義する。
given Monoid[Double] with
def empty = 0
def combine(x: Double, y: Double): Double = x + y
given Show[Double] with
def show(a: Double): String = a.toString()
ここから本題。
object Monoid:
// さっきまでに書いたコードは省略
import scala.compiletime.{erasedValue, summonInline}
import scala.deriving.Mirror
inline private def summonAll[T <: Tuple]: List[Monoid[?]] =
inline erasedValue[T] match
case _: EmptyTuple => Nil
case _: (t *: ts) => summonInline[Monoid[t]] :: summonAll[ts]
inline given derived[A](using m: Mirror.ProductOf[A]): Monoid[A] =
new Monoid[A]:
override def empty: A =
val instances = summonAll[m.MirroredElemTypes]
m.fromProduct(Tuple.fromArray(instances.map(_.empty).toArray))
override def combine(x: A, y: A): A =
val instances = summonAll[m.MirroredElemTypes]
val xs = x.asInstanceOf[Product].productIterator
val ys = y.asInstanceOf[Product].productIterator
val combineds = xs.zip(ys).zipWithIndex.map {
case ((xx, yy), i) =>
instances(i).asInstanceOf[Monoid[Any]].combine(xx, yy)
}
m.fromProduct(Tuple.fromArray(combineds.toArray))
一旦Monoidまで。
inline private def summonAll[T <: Tuple]: List[Monoid[?]] =
inline erasedValue[T] match
case _: EmptyTuple => Nil
case _: (t *: ts) => summonInline[Monoid[t]] :: summonAll[ts]
summonAllではTupleを受け取りADTのフィールドに対応するMonoidをリストで返している。
erasedValue[T]は何かというと
The erasedValue function pretends to return a value of its type argument T. Calling this function will always result in a compile-time error unless the call is removed from the code while inlining.
と公式にある通り、その型引数Tの値を返すふりをする。
ここではTupleが空の場合と値が存在する場合とでパターンマッチングを行う為に使用しており、下のパターンマッチング _: (t *: ts)
の t と ts にはそれぞれTupleの先頭の型と先頭以外の型のTupleが束縛されることになる。
summonInline
はスコープ内に存在する指定の方のインスタンスを取得するAPIで、scala.compiletime.summon
と同等の振る舞いをするが、呼び出しがインライン化されるまでインスタンスの取得は遅延される。
derivedは戻り値からわかるようにMonoidのインスタンスを返すものだが、ADTのインスタンスを返すという点で通常のインスタンス定義とは異なる。
また、メソッドの型からわかるようにderivedによってADTの型クラスインスタンスが生成される。
empty・combineの実装ではusing句で取得したMirror.ProductOf[A]
から Productを継承している抽象型A のメタ情報をsummonAllに渡す事で各フィールドに対応するMonoidインスタンスをリストで取得している。
m.fromProduct(Tuple.fromArray(instances.map(_.empty).toArray))
は見てなんとなくわかるとおりADTの各フィールドの値をsummonAllで取得したMonoidのemptyとして scala.deriving.Mirror.Product::fromProduct
から対象のADTを生成している。
val xs = x.asInstanceOf[Product].productIterator
val ys = y.asInstanceOf[Product].productIterator
val combineds = xs.zip(ys).zipWithIndex.map {
case ((xx, yy), i) =>
instances(i).asInstanceOf[Monoid[Any]].combine(xx, yy)
}
xs・ysではそれぞれxとyのフィールドの値をIterator[Any]
として取得しており、なぜAnyで取得するかというと instances: List[Monoid[?]]
から添字指定で取得した Monoid[?]
を Monoid[Any]
としてcombineを呼び出す為。circeでもこんな書き方だったので恐らくderivesを実装する際にはよく使うパターンぽい。
Monoidと同じようにShowを書いていく
object Show:
// 先ほど記述したものは省略
inline private def summonAll[T <: Tuple]: List[Show[?]] =
inline erasedValue[T] match
case _: EmptyTuple => Nil
case _: (t *: ts) => summonInline[Show[t]] :: summonAll[ts]
inline private def summonLabels[T <: Tuple]: List[String] =
inline erasedValue[T] match
case _: EmptyTuple => Nil
case _: (t *: ts) => constValue[t].asInstanceOf[String] :: summonLabels[ts]
inline given derived[A](using m: Mirror.ProductOf[A]): Show[A] =
new Show[A]:
override def show(a: A): String =
val instances = summonAll[m.MirroredElemTypes]
val lables = summonLabels[m.MirroredElemLabels]
val name = constValue[m.MirroredLabel]
val as = a.asInstanceOf[Product].productIterator
val shows = as.zipWithIndex.map { case (aa, i) =>
instances(i).asInstanceOf[Show[Any]].show(aa)
}
s"$name { ${lables.zip(shows).map { case (l, s) => s"$l: $s" }.mkString(", ")} }"
おおかた先ほどのMonoidと同じではあるが、今回ADTを文字列に変換するにあたりフィールドの名前が欲しかったので summonLabels
というメソッドを追加しているのと constValue[m.MirroredLabel]
でクラス名を取得している。
出力を見る
いい感じに出力できているか見てみる。
case class Position(x: Double, y: Double) derives Show, Monoid
@main def main: Unit =
val p1 = Position(2.0, 3.0)
val p2 = Position(1.0, 4.0)
println { (p1 |+| p2).show } // Position { x: 3.0, y: 7.0 }
期待通りですね!!
今回謎だったこと。
今回記事を書くにあたり↓のようにsummonAllの高カインドを抽象化しようと思ったらエラーになってしまった🥺
わかる方コメントいただけると嬉しいです🙏
inline def summonAll[T <: Tuple, F[_]]: List[F[?]] =
inline erasedValue[T] match
case _: EmptyTuple => Nil
case _: (t *: ts) => summonInline[F[t]] :: summonAll[ts, F]
/**
[error] 13 |inline def summonAll[T <: Tuple, F[_]]: List[F[?]] =
[error] | ^^^^
[error] | unreducible application of higher-kinded type F to wildcard arguments
*/