CLOVER🍀

That was when it all began.

Scala 2.10.0 Macros

いよいよ、
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

最初のマクロ

マクロを作成するには、

  1. マクロを定義したScalaコードを書く
  2. マクロを使用したScalaコードを書く

となり、この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

オマケ2

このサンプルを書いている時に、たまにハマったのがfscです。

マクロを変更して再コンパイルした時に、変更が認識されないという場面によく遭遇しました。仕方がないので、マクロを変更した場合は

$ fsc -shutdown

で1回落としてクラスファイルを消してから再度コンパイルしてました。