OBSOLETE
Eugene Burmako 著
Eugene Yokota 訳
型マクロ (type macro) はマクロパラダイスの以前のバージョンから利用可能だったが、マクロパラダイス 2.0 ではサポートされなくなった。 the paradise 2.0 announcement に説明と移行のための戦略が書かれている。
直観
def マクロがコンパイラが特定をメソッドの呼び出しを見つけた時にカスタム関数を実行させることができるように、型マクロは特定の型が使われた時にコンパイラにフックできる。以下のコードの抜粋は、データベースのテーブルから簡単な CRUD 機能を持ったケースクラスを生成する H2Db
マクロの定義と使用例を示す。
type H2Db(url: String) = macro impl
object Db extends H2Db("coffees")
val brazilian = Db.Coffees.insert("Brazilian", 99, 0)
Db.Coffees.update(brazilian.copy(price = 10))
println(Db.Coffees.all)
H2Db
マクロの完全なソースコードは GitHub にて提供して、本稿では重要な点だけをかいつまんで説明する。まず、マクロは、コンパイル時にデータベースに接続することで静的に型付けされたデータベースのラッパーを生成する。(構文木の生成に関してはリフレクションの概要にて説明する) 次に、NEW c.introduceTopLevel
API を用いて生成されたラッパーをコンパイラによって管理されているトップレベル定義のリストに挿入する。最後に、マクロは生成されたクラスのスーパーコンストラクタを呼び出す Apply
ノードを返す。注意 c.Expr[T]
に展開される def マクロとちがって型マクロは c.Tree
に展開されることに注意してほしい。これは、Expr
が値を表すのに対して、型マクロは型に展開することによる。
type H2Db(url: String) = macro impl
def impl(c: Context)(url: c.Expr[String]): c.Tree = {
val name = c.freshName(c.enclosingImpl.name).toTypeName
val clazz = ClassDef(..., Template(..., generateCode()))
c.introduceTopLevel(c.enclosingPackage.pid.toString, clazz)
val classRef = Select(c.enclosingPackage.pid, name)
Apply(classRef, List(Literal(Constant(c.eval(url)))))
}
object Db extends H2Db("coffees")
// equivalent to: object Db extends Db$1("coffees")
合成クラスを生成してその参照へと展開するかわりに、型マクロは Template
構文木を返すことでそのホストを変換することもできる。scalac 内部ではクラス定義とオブジェクト定義の両方とも Template
構文木の簡単なラッパーとして表現されているため、テンプレートへと展開することで型マクロはクラスやオブジェクトの本文全体を書き換えることができるようになる。このテクニックを活用した例も GitHub でみることができる。
type H2Db(url: String) = macro impl
def impl(c: Context)(url: c.Expr[String]): c.Tree = {
val Template(_, _, existingCode) = c.enclosingTemplate
Template(..., existingCode ++ generateCode())
}
object Db extends H2Db("coffees")
// equivalent to: object Db {
// <existing code>
// <generated code>
// }
詳細
型マクロは def マクロと型メンバのハイブリッドを表す。ある一面では、型マクロはメソッドのように定義される (例えば、値の引数を取ったり、context bound な型パラメータを受け取ったりできる)。一方で、型マクロは型と同じ名前空間に属し、そのため型が期待される位置においてのみ使うことができるため、型や型マクロなどのみをオーバーライドすることができる。(より網羅的な例は GitHub を参照してほしい)
機能 | def マクロ | 型マクロ | 型メンバ |
---|---|---|---|
定義と実装に分かれている | Yes | Yes | No |
値パラメータを取ることができる | Yes | Yes | No |
型パラメータを取ることができる | Yes | Yes | Yes |
変位指定付きの 〃 | No | No | Yes |
context bounds 付きの 〃 | Yes | Yes | No |
オーバーロードすることができる | Yes | Yes | No |
継承することができる | Yes | Yes | Yes |
オーバーライドしたりされたりできる | Yes | Yes | Yes |
Scala のプログラムにおいて型マクロは、type、applied type、parent type、new、そして annotation という 5つ役割 (role) のうちの 1つとして登場する。マクロが使われた役割によって許される展開は異なっている。また、役割は NEW c.macroRole
API によって検査することができる。
役割 | 使用例 | クラス | 非クラス? | Apply? | Template? |
---|---|---|---|---|---|
type | def x: TM(2)(3) = ??? | Yes | Yes | No | No |
applied type | class C[T: TM(2)(3)] | Yes | Yes | No | No |
parent type | class C extends TM(2)(3) new TM(2)(3){} | Yes | No | Yes | Yes |
new | new TM(2)(3) | Yes | No | Yes | No |
annotation | @TM(2)(3) class C | Yes | No | Yes | No |
要点をまとめると、展開された型マクロは型マクロの使用をそれが返す構文木に置き換える。ある展開が理にかなっているかどうかを考えるには、頭の中でマクロの使用例を展開される構文木で置き換えてみて結果のプログラムが正しいか確かめてみればいい。
例えば、 class C extends TM(2)(3)
の中で TM(2)(3)
のように使われている型マクロは class C extends B(2)
となるように Apply(Ident(TypeName("B")), List(Literal(Constant(2))))
と展開することができる。しかし、同じ展開は TM(2)(3)
が def x: TM(2)(3) = ???
の中の型として使われた場合は def x: B(2) = ???
となるため、意味を成さない。(ただし、B
そのものが型マクロではないとする。その場合は再帰的に展開され、その展開の結果がプログラムの妥当性を決定する。)
コツとトリック
クラスやオブジェクトの生成
StackOverflow でも説明したが、型マクロを作っていると reify
がどんどん役に立たなくなっていくことに気付くだろう。その場合は、手で構文木を構築するだけではなく、マクロパラダイスにあるもう1つの実験的機能である準クォートを使うことも検討してみてほしい。