いよいよ、
http://www.scala-lang.org/node/27499
に載っている新機能/実験的機能リストの中で最後になる、マクロです。コンパイル時に、ASTを触って何らかの処理をすることができます。
Macros
http://docs.scala-lang.org/overviews/macros/overview.html
日本語訳
http://eed3si9n.com/ja/node/61
このマクロ、前回のScala Reflectionの概要でもチラッと出てきていて、
http://docs.scala-lang.org/overviews/reflection/overview.html
「Compile-time Reflection」として名前だけ書かれています。コンパイル時に、ASTを操作できる機能ですよーと。
Scala Reflectionと違うのは、作用するのが実行時かコンパイル時かです。マクロはコンパイル時に作用します。が、ASTを触ることになるので、実際にはもうちょい面倒です…。
で、このマクロですが上記の概要ページに載ってるサンプルがあんまりよく分からなかったので、その他のページを参考にしながらいろいろ触ってみました。
参考にしたのは、上記ページにも載っている
Using macros with Maven or SBT
https://github.com/scalamacros/sbt-example
を取っ掛かりにしつつ、概要ページとにらめっこしながら頑張ってみましたと。
それから、ここも見た方がよいかも。
http://eed3si9n.com/ja/metaprogramming-in-scala-210
最初のマクロ
マクロを作成するには、
となり、この2つは分けておく必要があります。要は、マクロは先にコンパイルしておかなくてはならない、ということです。
では、マクロを書いてみます。
マクロを使用するには、マクロを定義するScalaソースに
import scala.language.experimental.macros
と書くか、コンパイルオプションに
-language:experimental.macros
を与える必要があります。自分は、import文で書きますが。
まずは、引数を取らずに単純に「My First Macro」と表示するだけのマクロを書いてみます。マクロの定義は、何かしらのobjectに書くようです。
import scala.language.experimental.macros import scala.reflect.macros.Context object FirstMacro { }
ここに、呼び出すマクロの関数定義を書きます。インターフェース的には、普通の関数と何ら変わりません。定義する関数名は、「printOnly」とします。
import scala.language.experimental.macros import scala.reflect.macros.Context object FirstMacro { def printOnly: Unit = macro printOnlyImpl }
ただし、関数定義の実体は書きません。macroに関数の実体定義を与えるだけになります。実体に名前は、「printOnlyImpl」とします。
続いて、実体の定義です。
def printOnlyImpl(c: Context): c.Expr[Unit] = c.universe.reify(println("My First Macro"))
引数は、必ずscala.reflect.macros.Contextです。また今回は戻り値がUnitなので、そのように宣言しますが、Context#Exprトレイトに型引数を与えたものが戻り値となります。
Context#universe#reifyはASTを簡単に作ってくれるメソッドです。
では、これをコンパイルしておきます。
$ fsc FirstMacro.scala
続いて、使う側に。
object UseMacro { def main(args: Array[String]): Unit = { import FirstMacro._ printOnly } }
こちらは、普通にメソッドとして使えばOKです。
では、コンパイル、実行。
$ fsc UseMacro.scala $ scala UseMacro My First Macro
動きましたね。
コンパイル時に「-Ymacro-debug-lite」オプションを付けると、詳細な情報というか、ASTが見れます。
$ fsc -Ymacro-debug-lite UseMacro.scala performing macro expansion FirstMacro.printOnly at source-/xxxxx/UseMacro.scala,line-5,offset=89 scala.this.Predef.println("My First Macro") Apply(Select(Select(This(newTypeName("scala")), newTermName("Predef")), newTermName("println")), List(Literal(Constant("My First Macro"))))
今回作成したマクロの完全な定義は、こうなります。
import scala.language.experimental.macros import scala.reflect.macros.Context object FirstMacro { def printOnly: Unit = macro printOnlyImpl def printOnlyImpl(c: Context): c.Expr[Unit] = c.universe.reify(println("My First Macro")) } }
ここでのポイントは、
といったところでしょうか。
引数を受け取るマクロ
続いて、引数を取るマクロを書いてみます。
お題は、「引数に文字列を取り、3回くり返したものして返す」とします。実体は、普通にStringOps#*でやりますが。
以下が定義になります。関数名は「triple」とします。
import scala.language.experimental.macros import scala.reflect.macros.Context import java.text.SimpleDateFormat import java.util.Date object FirstMacro { def printOnly: Unit = macro printOnlyImpl def printOnlyImpl(c: Context): c.Expr[Unit] = c.universe.reify(println("My First Macro")) def triple(msg: String): String = macro tripleImpl def tripleImpl(c: Context)(msg: c.Expr[String]): c.Expr[String] = { import c.universe._ val Literal(Constant(m: String)) = msg.tree c.Expr(Literal(Constant(m * 3))) // 上記は、以下でもOK // c.literal(m * 3) // さらに、全部まとめて以下でもOK // c.universe.reify(msg.splice * 3) } }
tripleのシグネチャ自体は、普通に引数を取る通常の関数定義です。ただ、引数を取ってもmacroに関数定義を渡すところは変わりません。
で、実体の定義ですがこうなっています。
def tripleImpl(c: Context)(msg: c.Expr[String]): c.Expr[String] = { import c.universe._ val Literal(Constant(m: String)) = msg.tree c.Expr(Literal(Constant(m * 3))) // 上記は、以下でもOK // c.literal(m * 3) // さらに、全部まとめて以下でもOK // c.universe.reify(msg.splice * 3) }
今度は値を返すので、戻り値の型が
c.Expr[String]
となっています。また引数は、カリー化した上でContext#Expr型で受け取ることになります。
msg: c.Expr[String]
で、ここから引数に渡されてきたStringを抜き出したいところですが、変数msgはStringではないので、分解します。Context#Expr#treeでASTが取得できるので、これとUniverse#LiteralとUniverse#Constantを使って中の値を取得します。
import c.universe._ val Literal(Constant(m: String)) = msg.tree
この辺りのコードを書く時は、Context#universeをimportしておくのが通例みたいです。
これで、変数mに引数で渡されたStringが入ります。
あとはこれにStringOps#*(3)をして、ASTを作ってExprでラップして返します。
c.Expr(Literal(Constant(m * 3)))
これで、コンパイル時にASTを操作していることになっています。引数を受け取らない方のマクロも同じ話なのですが、あちらは戻り値がないマクロだったので、戻り値のある定義で書いた方がよいかなぁ〜と。
なお、ソースコメントにも書いていますが、最後の
c.Expr(Literal(Constant(m * 3)))
は
c.literal(m * 3)
に置き換えられます。
さらに、ここまでの処理を全部まとめて
c.universe.reify(msg.splice * 3)
にも置き換えられます。Expr#spliceで中の値が取れます。ただ、これはContext#universe#reifyと一緒に使うべきだそうな。
では、作ったマクロを使ってみます。
object UseMacro { def main(args: Array[String]): Unit = { import FirstMacro._ printOnly println(triple("Hello")) } }
実行すると
$ scala UseMacro My First Macro HelloHelloHello
となります。動いてますね。
ちなみに、scalacとかで「-Xprint」とかでコンパイル結果を見ると分かりますが
$ scalac -Xprint:jvm UseMacro.scala [[syntax trees at end of jvm]] // UseMacro.scala package <empty> { object UseMacro extends Object { def main(args: Array[String]): Unit = { scala.this.Predef.println("My First Macro"); scala.this.Predef.println("HelloHelloHello") }; def <init>(): UseMacro.type = { UseMacro.super.<init>(); () } } }
と、コンパイル結果にはすでに「Hello」が3回入っていることが分かります。
あくまで、「コンパイル時にASTを触っている」というところがポイントです。
あと、可変長引数を取るマクロも書いてみました。
def varargs(args: Int*): Int = macro varargsImpl def varargsImpl(c: Context)(args: c.Expr[Int]*): c.Expr[Int] = { import c.universe._ val sum = args.toList.map { i => val Literal(Constant(iv: Int)) = i.tree iv }.sum c.literal(sum) }
Intを受け取って、合算して返すだけです。Listを返そうと頑張ってみましたが、ちょっと挫折しました。
ここまで、主に触ってきたクラスやトレイトは
Context
http://www.scala-lang.org/api/current/index.html#scala.reflect.macros.Context
Universe(*scala.reflect.macrosパッケージです)
http://www.scala-lang.org/api/current/index.html#scala.reflect.macros.Universe
Expr
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Exprs$Expr
Literal
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Trees$Literal
Constant
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Constants$Constant
中心にいるのは、UniverseとTreesなんでしょうね。
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Trees
ここの定義に、すごい既視感があります…。
Scala Reflectionといい、マクロといい、大変ですね…。どれだけ使う人いるんでしょ?AST触るのは、かーなり面倒。
とりあえず、ひとまず新機能リストに載っているものは、だいたい触ってみた感じですかね。載っているものは…。
オマケ
マクロを有効活用したものがあまり思いつかなかったので、C言語の「__LINE__」みたいなものを書いてみました。
意気込んで始めたものの、意外とあっさり情報が取得できてしまってちょっとビックリ。
import scala.language.experimental.macros import scala.reflect.macros.Context import java.text.SimpleDateFormat import java.util.Date object CLikeMacro { def __LINE__ : Int = macro srcLineImpl def srcLineImpl(c: Context): c.Expr[Int] = c.literal(c.enclosingPosition.line) def __FILE__ : String = macro fileImpl def fileImpl(c: Context): c.Expr[String] = c.literal(c.enclosingUnit.source.file.name) def __METHOD__ : String = macro methodImpl def methodImpl(c: Context): c.Expr[String] = c.literal(c.enclosingMethod.symbol.name.decoded) def __DATE__ : String = macro dateImpl def dateImpl(c: Context): c.Expr[String] = c.literal(new SimpleDateFormat("yyyy/MM/dd").format(new Date)) def __TIME__ : String = macro timeImpl def timeImpl(c: Context): c.Expr[String] = c.literal(new SimpleDateFormat("HH:mm:ss.S").format(new Date)) }
使ってみましょう。
object UseMacro { def main(args: Array[String]): Unit = { import FirstMacro._ printOnly println(triple("Hello")) println(varargs(1, 2, 3, 4, 5)) import CLikeMacro._ println("Current Line => " + __LINE__) println("Current Source => " + __FILE__) println("Current Method => " + __METHOD__) println("Current Line Complied Time => " + __TIME__) println("Current Line Complied Date => " + __DATE__) } }
実行。
$ scala UseMacroMy First Macro My First Macro HelloHelloHello 15 Current Line => 12 Current Source => UseMacro.scala Current Method => main Current Line Complied Time => 23:09:15.600 Current Line Complied Date => 2013/01/23
時間は、コンパイル時に埋め込んでいるので何回実行しても、同じ時間が出力されます。
ソース関連の情報は、Contextから取得できます。
Position
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Position
CompilationUnit
http://www.scala-lang.org/api/current/index.html#scala.reflect.macros.Universe$CompilationUnit
TreeContextApi
http://www.scala-lang.org/api/current/index.html#scala.reflect.macros.Universe$TreeContextApi