Scala

Macro Paradise 入門

More than 1 year has passed since last update.

どうやら気付かない間に Macro Paradise 3.0.0 が公開されていた ようですが、この記事は 2.1.0 に関する記事です。なお、3.0.0 でも旧来の機能は提供されているようなのでこの記事はおそらく有効です。(6/8追記)

Macro Paradise plugin (執筆時 versin 2.1.0) は Scala 2.10 で実験的に導入されたマクロ機構の中でも特に実験的な機能を提供するプラグインです。とはいえ、現在ではマクロアノテーションだけが唯一現存する機能です。過去には様々な機能が搭載されていたようですが、当分の間これ以外の機能は提供されない見込みです。この記事では、メソッドの引数と返り値を出力するマクロアノテーションの実装を通してこのプラグインの使い方を紹介します。

Macro Paradise plugin は、SBTを使うなら以下のように書くことで簡単に利用できます。CPS plugin など他のプラグインと干渉する場合もありますのでプラグインの追加順序にはよく気をつけることをおすすめします。


build.sbt

addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)


scala.meta の開発が進むなど現行の Scala macro の先行きはあまり良いものではないです。一方で現行のマクロシステムも当分の間現状のままサポートされるようなので覚えておいても損はないことでしょう。


マクロアノテーション

まずマクロアノテーションの概要を説明します。マクロアノテーションは、特別なアノテーションで修飾された定義文をマクロ展開する機能です。例えば、以下のとき@mcrをマクロアノテーションだとすると、修飾されているclass Clsdef method() = ???が展開対象の構文木となります。

// クラス

@mcr class Cls
// メソッド
@mcr def method() = ???

マクロアノテーションを使うと、クラス・メソッドといった通常のScalaマクロよりも大きな範囲でマクロ展開ができます。マクロアノテーションの簡単な例がPluginのレポジトリのテストにいくつかあるので見てみるとよいでしょう。例えば、@helloのようにクラスにhelloメソッドを追加するあまり役に立たない例もあれば、@kaseのようにケースクラスと同様の自動生成機能を実現した本格的な例もあります。

注意点ですが、型に対するアノテーションはマクロアノテーションにはなりません。つまり以下の@mcrのように型に対して修飾してもval a: Int@mcr = ???が展開されることはありません。1

val a: Int@mcr = ???


例: メソッドの引数・返り値を出力するマクロアノテーション

それでは、例を紹介します。今回作るマクロアノテーションはメソッドの引数と返り値を出力する@loggerです。例えば、以下のようにidentityメソッドを修飾すると出力が以下のようになります。

@logger def identity[A](a: A) = a

identity(10)

$ identity (10) = 10


実装の概略

これを次のようにして実装します。まず、処理の本体を別メソッド$logに分けます。2 これはreturn式で挿入した出力をスキップされることを防ぐためです。3 次に引数と返り値をprintで出力します。identityの変換後のイメージを示すと以下のようになります。とても簡単ですね。

def identity[A](a: A) = {

val r = identity$log[A](a)
print("identity (")
print(a)
print(") = ")
println(r)
r
}
def identity$log[A](a: A) = a


実装の詳細

それではマクロアノテーションの実装の詳細に入ります。マクロアノテーションの実装は、アノテーションと展開メソッドの2つのパートからなります。

まず、アノテーションです。以下のようになります。

class logger extends scala.annotation.StaticAnnotation {

def macroTransform(annottees: Any*): Any = macro loggerBundle.impl
}

Java同様、Scalaでもアノテーションはクラスです。マクロアノテーションはそのなかでもscala.annotation.StaticAnnotationを継承した、コンパイル後のバイナリに影響与えるタイプのアノテーションである必要があります。また、def macroTransform(annottees: Any*): Anyメソッドを定義する必要もあります。右辺の書き方は普通のマクロと同様にmacro [展開メソッド名]となります。これらの条件を満たしていると、Paradiseプラグインによってマクロアノテーションとみなされ、被修飾定義文がマクロ展開されるようになります。

次に変換メソッドです。以下のようになります。

class loggerBundle(val c: scala.reflect.macros.whitebox.Context) {

import c.universe._

def impl(annottees: Tree*): Tree = {
val DefDef(mods, name, tparams, vparamss, tpt, rhs) :: Nil = annottees
val newName = TermName(name + "$" + "log")
val list = List(
q"val r = $newName[..${tparams.map(_.name)}](...${vparamss.map(_.map(_.name))})",
q"print(${name + " ("})",
q"print(..${vparamss.flatten.map(_.name)})",
q"print(${") = "})",
q"println(r)",
q"r"
)
val created = DefDef(mods, name, tparams, vparamss, tpt, q"..$list")
val copied = DefDef(mods, newName, tparams, vparamss, tpt, rhs)
q"$created; $copied; ()"
}
}

変換メソッドは object もしくは 2.11 で導入された macro bundle と呼ばれる形式のクラスでのみ定義することができます。今回は自分の好みにより macro bundle 形式を使います。Macro bundle は Context型のパラメータをちょうど1個だけ取り他にはパラメータがないクラスです。この形式には、import c.universe._import c.internal._などといったimport文のボイラープレートを除去できる、展開メソッドの引数が減るといった利点があります。一方で、IntelliJで間違ったエラーが検出される、モジュール化の時にパス依存型に悩まされるといったクセがあるので使用には気をつけましょう。

Context型ですが、2.11ではscala.reflect.macrosパッケージのblackbox.Contextwhitebox.Contextの2種類から選ぶ必要があります。通常のScalaマクロではどちらかという情報が展開結果に影響するので重要なのですが、マクロアノテーションという用途では一切使わないようなので好みで選べばいいと思います。なお、APIの差も implicit 関連でマクロアノテーションでは使えないので気にしなくてよいです。なお、Macro Paradise 公式のサンプルでは scala.reflect.macros.Context を使っていますが、これは 2.10 のマクロ用で古いので使わないようにしましょう。

実際の展開メソッドはアノテーションの方で指定したようにimplメソッドになります。引数は可変長引数で修飾されている定義文の構文木(複数)が渡されます。実は多くの場合はちょうど1個です。クラスの場合だけコンパニオンオブジェクトが同時に渡されるということになっているので2個になります。こんなことに可変長引数を使うなんてオーバーキルですよね。引数の数を見てよしなにやってほしいものです。また返り値は変換後の構文木となります。実はここで複数返すこともでき、上の実装ではそれを使っています。Block型の構文木オブジェクトに複数の定義文を詰め込むとBlockを剥がした上で並べられた順に差し込まれます。

なお、Macro Paradise 公式のサンプルではTreeの代わりにExpr[A]を使っていますが、これはどちらを使ってもマクロシステム側がよしなにしてくれるということになっています。しかし、Expr[A]系のAPIは使いにくい上に結局内部でTreeを使うことになるのと、2.11のマクロではもっぱらTreeだけが使うのが一般的になったのでTreeにすべきだと思います。

実際の変換については普通のScalaマクロと同じようにして上の概要にそって作っただけなので説明を省略します。ただし後述する型情報、シンボル情報がほぼ使えないということには注意が必要です。

これらをまとめると、以下の URL のようになります。

https://github.com/hiroshi-cl/scala-examples/blob/master/scala-method-logger/src/main/scala/Macros.scala


実行例

テストコードを以下に置きました。

https://github.com/hiroshi-cl/scala-examples/blob/master/scala-method-logger/src/test/scala/Test.scala

これをsbt test:runで実行すると以下のような出力が得られます。

hello (10) = 10

10
bye ((20,30)) = 20
20
ret
ret (30) = 30
30
poly (40)


宿題

今回の実装はどうせ大した用途には使われないだろうという予測の下かなり雑に作ってあります。より多くの環境で動くように自分で修正を試みてみると良いでしょう。


マクロアノテーションの注意点

このように強力なマクロアノテーションですが、いくつか注意点もあります。


型情報がない

Scala のマクロでは事前に構文木の型チェックを行うというのがウリなのですが、マクロアノテーションではその原則が崩れます。基本的に型情報はついていません。その上、Scala マクロには構文木の型チェックをするAPIが用意されているのですが、それを呼ぶと多くの場合コンパイラが落ちる(!?)というオマケがついてきます。ここから、本家に取り込まれていない理由の一端がわかるのではないでしょうか?(本当のところは知りませんが)

型チェックが行われていないというのは、単にチェックが不十分という以外にも様々な影響があります。変数名・メソッド名参照の名前解決、デフォルト引数・暗黙引数の補完、applyの挿入など型チェック時に行われる構文(?)糖の脱糖、マクロ展開などといったことが行われていません。このため、マクロアノテーションでは普通のScalaマクロを挿入するだけに留めて、より高レベルの展開は遅延させるといったテクも使うと良いでしょう。

一方で、型がついていないというのはメリットでもあります。ScalaマクロではLispで見られるマクロの例の多くを型チェッカが弾いてしまい実現することができません。マクロアノテーションを使えば、事前の型チェックが入らないのでそれらの幾つかを実現できる…かもしれません。とても頑張れば。もちろん、IDEの支援の外に出てしまうという危険性をはらんでいるという欠点もあるので乱用には気をつけましょう。


シンボルがない

上の型情報がないとも強く関連するのですが、マクロアノテーションで与えられる構文木にはシンボルもありません。シンボルは各識別子を区別するために使われるオブジェクトです。同じ名前の変数が存在してshadowingが起きているときなど際どい例でも普通に書いたマクロ展開が動作するなど、Scalaのマクロシステムでhygiene性を保つために重要な役目を果たしています。ところが、マクロアノテーションではこの情報がないため、代わりに直接名前を使わざるを得ず、hygiene性の問題に直に晒されることになります。

対処としては、リネームを頑張ったり、freshNameの利用の徹底といったことには通常のマクロ以上に気を使うようにしましょう。上の実装例でも通常シンボルを用いて参照を作るところ、パラメータ名を直接参照しているため、本来はこの問題が起きていないか十分気をつける必要があります。


今回のソースコード

今回のソースコードは以下のURLにおいてあります。SBTを使っているのでsbt test:runでプロジェクトに同梱されているサンプルの実行結果を見ることができます。

https://github.com/hiroshi-cl/scala-examples/tree/master/scala-method-logger





  1. Inverse macro を使うと良いことがあるかもしれません。 



  2. 本当はfreshNameを使って名前が被らないように配慮する必要がありますが、どうせそんな事故は起きないので忘れます。 



  3. throwとかshiftとか使われると困ったことになりますが忘れます。