Scala初心者がShapelessのHListでderivingをどうやるのかを学んだ話 - だいたいよくわからないブログでは、HListに対しての演算を定義すればcase classに対しての演算をいちいち定義する必要がない。(case classをHListに変換→HListで演算→演算後のHListをcase classに戻せばよい。)ということを紹介しました。
しかし、紹介したコードではcase classをHListにするためのfrom
とto
メソッドをいちいち手書きする必要がありました。これでは本末転倒ですので、今回はfrom
とto
の自動生成をマクロで行いたいと思います。
とりあえずbuild.sbt
です。
name := "Iso" version := "1.0" scalaVersion := "2.11.6" libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
同型射 Iso
case classとHListのように、どっちも相互に変換可能だし、どっちで計算しても答えは変わらないよーという性質をもったものを同型写像といいます。これにちなんでIso(Isomorphismの略?)という名前が付くのが一般的なようですので、本記事でもfrom
とto
メソッドを持ったtraitをIso
という名前で呼びたいと思います。
Iso
の定義はこんな感じです。
trait Iso[T, U] { def to(t: T): U def from(u: U):T }
T => U
と U => T
がありますね。
ここまでは前回と同じです。
問題は、どのようにIso[T, U]
を自動生成するかです。
macro
Iso
の生成にはいくつかの方法があるらしいのですが、今回はマクロを利用したいと思います。
あと、HListでやる前にtupleで練習しよ(ง ˘ω˘ )วと思ったら動かなさすぎてげんなりしたのでtuple版の実装です(◞‸◟)
traitの定義も含めた実装はこんなかんじです。
package Deriving import scala.language.experimental.macros import scala.reflect.macros.whitebox trait Iso[T, U] { def to(t: T): U def from(u: U):T } object Iso { implicit def materializeIso[T, U]: Iso[T, U] = macro impl[T, U] def impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: whitebox.Context): c.Expr[Iso[T, U]] = { import c.universe._ // case classのクラス名 val caseClassSym: c.universe.Symbol = c.weakTypeOf[T].typeSymbol if (!caseClassSym.isClass || !caseClassSym.asClass.isCaseClass) c.abort(c.enclosingPosition, s"$caseClassSym is not a case class") // case classのフィールドに関するobject object Fields { // 各フィールドのシンボル val syms: List[TermSymbol] = caseClassSym.typeSignature.decls.toList.collect { case x: TermSymbol if x.isVal && x.isCaseAccessor => x } // List(Int, String, Int) のようなフィールドの型名を並べたリスト val types: List[Tree] = syms map (f => tq"${f.typeSignature}") // Lost(t.foo, t.bar, t.baz) のように 「t.フィールド名」 と並べたリスト (tはtoで使えるように、toの引数名に合わせている) // 本当はt.nameとしたいが、t.`foo` となってしまうので、 t.fooにするためにstring化→trimして回避している val names: List[Tree] = syms map (f => q"t.${TermName(f.name.toString.trim)}") } import Fields._ // case classと同型なタプルの型 // Tuple3[Int, String, Int]など def tupleType: Tree = tq"(..$types)" // case classと同型なタプルのインスタンス // (1, "aaa", 2)など def toImpl: Tree = q"(..$names)" // タプルと同型なcase classのインスタンス // case classのフィールドが0個のときはapply.tupledが無いので直接インスタンス化する // Foo(1, "aaa" 2)など def fromImpl: Tree = q"${if(syms.isEmpty) q"${caseClassSym.companion}.apply" else q"${caseClassSym.companion}.tupled(u)"}" // Iso[T, U]の無名サブクラスを定義してnew val evidence: Tree = q""" final class $$anon extends Iso[$caseClassSym, $tupleType] { def to(t: $caseClassSym): $tupleType = $toImpl def from(u: $tupleType): $caseClassSym = $fromImpl } new $$anon """ c.Expr[Iso[T, U]](evidence) } }
https://gist.github.com/matsu-chara/dde005b46c5a7218ace9#file-iso-scala
なかなか動かなくてコメントを書きまくったので、何をしているかなんとなく想像できるのではないでしょうか。
この実装を使うと、下記のようにネストしたcase classもtupleに変換することができます。
package Deriving // (だいたい)任意のcase classをマクロでtupleに変換 object DerivingTest extends App { // テスト用ケースクラス case class Bar() case class Foo(i: Int, s: String, d: Double) case class Baz(i: Int, s: String, d: Double, f: Foo) // isoインスタンス val barIso = implicitly[Iso[Bar, Unit]] val fooIso = implicitly[Iso[Foo, (Int, String, Double)]] val bazIso = implicitly[Iso[Baz, (Int, String, Double, Foo)]] // フィールド数0のクラス val bar : Bar = Bar() val tupleFromBar: Unit = barIso.to(bar) val barFromTuple: Bar = barIso.from(tupleFromBar) println(s"$tupleFromBar, $barFromTuple") // 普通のクラス val foo = Foo(23, "foo", 4.3) val tupleFromFoo = fooIso.to(foo) val fooFromTuple = fooIso.from(tupleFromFoo) println(s"$foo, $tupleFromFoo, $fooFromTuple") // ネストした型 val baz = Baz(1, "b", 2.3, fooFromTuple) val tupleFromBaz = bazIso.to(baz) val bazFromTuple = bazIso.from(tupleFromBaz) println(s"$baz, $tupleFromBaz, $bazFromTuple") } }
https://gist.github.com/matsu-chara/dde005b46c5a7218ace9#file-derivingtest-scala
あとはtupleNに対して演算を定義してやればcase class(フィールド数N)に対しての型クラスが自動導出できるはずです。 一つの型クラスに対して高々22個なのでやろうと思えば(フィールド数22個以内なら)できるところまで来たと思います。
まとめ
実を言うと、この実装はSI-7470 implements fundep materialization by xeno-by · Pull Request #2499 · scala/scala · GitHubで挙げられている例のパクリなんですが、scala/Macros_1.scala at 7b890f71ecd0d28c1a1b81b7abfe8e0c11bfeb71 · scala/scala · GitHubでは from
が実装されていないので、完全版という意味合いと、準クォートを使っているので少し読みやすいというメリットがあります。
しかし、変換後の型を自分で書かないといけないのは面倒ですね(´・_・`)
to
だけなら推論してくれるんですがfrom
は返り値の型(どのcase classか)を指定しなければならないのでちょっと厳しそうです。もしかしたら何か方法があるかもしれません・・・。
次はもうちょい頑張って、HListへのIso traitを書いてshpelessのような自動導出のコード例が書けたらなと思います。