田中 貴也と申します。プログラミング歴は一年半くらいで、scalaは趣味で一年ほど触ってます。
shapelessを読もうとしていたらmacroだらけで意味不明だったのでmacroを勉強しようと思った。
(scalaでは)コンパイル時に構文木を受け取り、処理できる つまりコードを置換する機構
macro本体の実装は利用するコードより先にコンパイルされていなければいけないのでsbtで使う時は プロジェクトを分けて先にマクロ側が先にコンパイルされるようにする。
lazy val user = (project in file("user")) .dependsOn(macroImpl) lazy val macroImpl = (project in file("macro-impl")) .settings( libraryDependencies ++= Seq( "org.scala-lang" % "scala-reflect" % scalaVersion.value ) )
object MacroImpl{ def apply[A, B](a: A): B = macro impl def impl[A :c.WeakType](c: Context)(a: c.Tree): c.Tree = { import c.universe._ val result = a match { case 抽象構文木のパターン => 返す値の抽象構文木 case _ => c.abort(c.enclosingPosition, "マッチしなかったらコンパイルエラー") } } }
c.enclosingPositionはマクロが展開されている場所c.abortでコンパイルエラーにできる。c.infoで警告も出せる。
例 def macroに渡された値がリテラルだったらそのまま返しリテラルではなかったらコンパイルエラーにするmacro
object isLiteral{ def apply[A](a: A): A = macro impl[A] def impl[A](c: Context)(a: c.Tree): c.Tree = { import c.universe._ a match { case Literal(l) => Literal(l) case _ => c.abort(c.enclosingPosition, s"$a is not literal") } } }
単純な例では構文木を手で組み立てれば良いが少し複雑になると途端に手につけられなくなる
//二つの整数を足してprintする Apply(Ident(TermName("println")), List(Apply(Select(a, TermName("$plus")), List(b))))
????
準クォートを使えばかなり見やすくなる
ASTの生成をscalaのコードに似たdslで組み立てられる機能 上記の場合
//二つの整数を足してprintする Apply(Ident(TermName("println")), List(Apply(Select(a, TermName("$plus")), List(b)))) //準クォートでの表記 q"println($a + $b)"
準クォートがどのようなASTに変換されるのかを見るには
showRaw(q"")
で確かめる事ができる。
showRaw(q"""println("hello world")""") //Apply(Select(Select(This(TypeName("scala")), scala.Predef), TermName("println")), List(Literal(Constant("hello world"))))
準クォートで抽出/パターンマッチもできる
val q"class $name { ..$body }" = tree
object MacroImpl{ def apply[A](a: A): Any = macro impl def impl[A :c.WeakType](c: Context): c.Tree = { import c.universe._ //型の取得 val tpe = weakTypeOf[A] //シンボルの取得 val sym = tpe.typeSymbol } }
typeSymbolではかなり詳細な型情報を取得できる。 isSealed, isCaseClass, isMethodなどかなり便利
//case class であるか判定 val sym = weakTypeOf[A].typeSymbol val clazz = if (sym.isClass) sym.asClass else c.abort(c.enclosingPosition, s"$sym is not class") c.Expr[Boolean](q"${clazz.isCaseClass}")
以上を踏まえて渡された値がcase class またはobjectのみで定義されたsealed trait/classか判定するマクロ (つまり代数的データ型かどうか判定する)
def impl[A: c.WeakTypeTag](c: Context): c.Expr[Boolean] = { import c.universe._ val tpe = weakTypeOf[A] val sym = tpe.typeSymbol def abort(msg: String) = c.abort(c.enclosingPosition, msg) def classSym(tpe: Type): ClassSymbol = { val sym = tpe.typeSymbol if (!sym.isClass) abort(s"$sym is not a class or trait") sym.asClass } def isObjectLike(sym: ClassSymbol) = sym.isModuleClass def isSealedHierarchyCaseClassOrCaseObject(symbol: ClassSymbol): Boolean = { def helper(classSym: ClassSymbol): Boolean = { //同じファイルから継承されているサブクラスを取得 classSym.knownDirectSubclasses.toList forall { child0 => println(child0.asClass.isCaseClass) val child = child0.asClass //再起的に検索 isObjectLike(child) || child.isCaseClass || (child.isSealed && helper(child)) } } symbol.isSealed && helper(symbol) } val symbol = classSym(tpe) c.Expr[Boolean](q"${isSealedHierarchyCaseClassOrCaseObject(symbol)}") }
実演
reveal.jsではまった事
デフォルトでsyntax highlightが効かない!
<code class="scala"> </code>
をindex.htmlに含めないとsyntax highlightが効かない。。