Scala.meta 1.0.0 がリリースされたみたいですね!それはさておきこの記事では、最近登場したMacro Paradise 3.0.0-M{1,2,3}の紹介します。
Macro Paradise 3.0.0-M{1,2,3} は SBT で以下のように書くことで使えます。
addCompilerPlugin("org.scalamacros" % "paradise" % "3.0.0-M3" cross CrossVersion.full)
Scala meta
Scala meta は、Scala コードを解析したり、自動生成したりする人のためのフレームワークです。アプリケーションとしてコードフォマッターの Scalafmt や、静的解析器の Codacy などがあるそうです。最終的には、コンパイラやマクロなどもサポートする予定のようですが、1.0.0 では Scala の構文木を表現するためのクラス群のみが提供されています。今のところは Scala meta に対応したマクロを提供するプラグインなどはなさそうです。(あったら教えて下さいな ^^)
Macro Paradise 3.0.0-M3
Macro Paradise 3.0.0-M3 では Scala meta で提案されている新しいスタイルのマクロを提供する…と思いきや、しません。Macro Paradise の機能は相変わらずマクロアノテーションのみです。では何が新しいかというと、まだ 1.0.0 には入っていない新しい Scala meta 形式でマクロアノテーションが作れる点です。
以下の例は、公式にある例で、 object に main
メソッドを追加します。
import scala.meta._
class main extends scala.annotation.StaticAnnotation {
inline def apply(defn: Any) = meta {
val q"object $name { ..$stats }" = defn
val main = q"""
def main(args: Array[String]): Unit = { ..$stats }
"""
q"object $name { $main }"
}
}
以下に示す 2.1.0 風と比較すると幾つかの点が異なります。
class main extends scala.annotation.StaticAnnotation {
def macroTransform(annottee: Any*): Any = macro Bundle.impl
}
import scala.reflect.macro.whitebox.Context
class Bundle(c: Context) {
import c.universe._
def impl(annottee: Tree*): Tree = { ... }
}
まず、マクロメソッドがインターフェースを担う macroTransform
と変換メソッド (ここでは impl
) に分かれていたのが apply
1つに集約されました。また、inline
とmeta
の2つのキーワードが導入されました。逆に言うと、この inline
を使って定義された apply
メソッドをもつ StaticAnnotation
のサブクラスがプラグインにマクロアノテーションと認識されます。
仕組みを説明しましょう。構文解析フェーズで inline
を検出すると以下のように機械的に書き換えます。
class main extends scala.annotation.StaticAnnotation {
@scala.meta.internal.inline.inline def apply(defn: Any) = ???
}
object main$impl {
def apply$impl(defn: metaTree) = {
val q"object $name { ..$stats }" = defn
val main = q"""
def main(args: Array[String]): Unit = { ..$stats }
"""
q"object $name { $main }"
}
}
これによって、2.1.0 と同様の形式に直してしまうわけですね。
なお、meta
は今のところは何の機能も果たしていませんが、将来的には必要な implicit を暗黙のうちに import させるブロックとして機能する予定のようです。ちなみに、厳密にはmeta
はキーワードではなく、クロージャを取るメソッドという扱いのようですが、現行の Scala には暗黙の import をするブロックをサポートする機能が存在しないのでどうするつもりなのか興味があります。1
M1/M2/M3 の違い
M1 では M2 以降と違い以下のように object 内に定義していました。
import scala.meta._
object main {
inline def apply()(defn: Any) = meta {
val q"object $name { ..$stats }" = defn
val main = q"""
def main(args: Array[String]): Unit = { ..$stats }
"""
q"object $name { $main }"
}
}
これまでのマクロでは object か macro bundle という特別な形式の class にしか定義することができなかったためだと思われます。しかし、よく知っている人はわかるように object では @main
は invalid なアノテーションで、相当不自然です。そのせいか、M2 からは普通のアノテーションに置き換えられました。
この副作用として、アノテーションの引数をマクロ内で直接取れるようになりました。引数というのは @compileTimeOnly("これはコンパイル時限定")
のようなものの "これはコンパイル時限定"
の部分のことです。2.1.0 では、c.prefix
を呼ぶとアノテーション全体分の構文木が取れるので、それを自分でバラす必要がありました。割と面倒でした。
M2 では、今回紹介したようなマクロアノテーションに仕様変更が行われました。
M3 では、def
やval
のようにトップレベルでないものにもアノテーションが付けられるようになりました。実は M2 ではトップレベルしか対応していなかったのですね。はい。よく見るとissue#3にも書かれていました。実はこのバージョンアップは私の犯行です。前回の記事で新バージョンがあるという事実を見逃したリベンジをしようとしてできなかったので、ついカッとしてプルリクを投げてしまいました。反省はしています。
Scala meta ではまったところ
-
Param
はTerm
じゃないのでArg
にならない-
Defn.Def
のパラメータを使ってメソッド適用の木を作るのがめんどい
-
-
Defn
(メンバ?) とDecl
(ローカル?) が違う-
Defn
はBlock
に入れられないので 2.1.0 の複数返すテクが使えない…
-
- 依存ライブラリに指定されている 0.20.0 では
...
記法がバグってた- 1.0.0 では直っているらしいけどまだ確認できていない
-
そのような機能を持つプロトタイプとしてInverse macro の副産物としてできたプラグインがあるので今度紹介記事を書きたいと思います。 ↩