3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Scala3 derives 解説

Last updated at Posted at 2023-12-11

はじめに

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
*/
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?