Scalaのマクロの基礎。評価タイミング、評価回数と、健全性

Scalaのマクロというより、一般的にマクロに共通する基本であり重要な部分です。それをScala使って説明するだけです。


Scalaのマクロは、未だexperimentalという位置づけで、他の機能に比べれば仕様やAPIが変わりやすい状態です。そして、機能が搭載されてからあまり時間が経っていないこともあって*1あまり一般的に使われているとはいえない状態でしょう。しかし、Cなどのマクロとは違い、Scalaのものはある程度は本格的にコンパイル時に抽象構文木を自由にいじれるものであり、使いこなせるようになってくるとなかなかおもしろいです。

マクロというと、ある程度の人はLispを思い浮かべると思いますが(?)、先ほど書いた「本格的に抽象構文木いじれる」という点はまさにLispと共通する部分もあります(もちろん異なる部分も多くあります)。
つまり、これから説明することは、On Lisp*2

On Lisp

On Lisp

を読めば、本の前半〜中盤までには当たり前のように書いてあることです。よって、そういう意味においては、いまさらマクロの「評価タイミング、評価回数と、健全性」を解説しても何も新規性はないです。しかし、Scalaに詳しいかつ、マクロにも詳しい人は多くないと思うので、Scalaにもマクロにもあまり詳しくない人でもある程度理解できるように、簡単な例を出して説明していこうと思います。



前置きが長くなってしまいました。そろそろ説明に入っていきます。最初の例としてはassertEqualという単純なマクロを作っていきます。
一つ前提として、Scala2.11で説明します。Scala2.10と2.11では、かなりマクロ関連で使える機能が異なり、圧倒的にScala2.11のほうが便利だし、クロスビルドするマクロを書くのは大変なので、皆さんにも、Scalaでマクロ入門するなら、(2.10を切り捨てて)Scala2.11でやることをおすすめします。



さて、assertEqualは何をするマクロかというと

  • 引数を2つとる
  • それら2つの引数が等しいかどうかを検査し、等しくなければエラーを投げる

という、メソッド名の通りの単純なマクロです。シグネチャを示すと以下です*3

def assertEqual(a: Any, b: Any): Unit = // 実装はこれから

さて、上記の仕様ならわざわざマクロにする必要ありませんね。ここで一つマクロでないと不可能な重要な要件を加えると

  • マクロでコンパイル時に抽象構文木を取得しておき、等しくなかったエラーの場合には、その抽象構文木からわかりやすいエラーメッセージを生成する

というものです。言葉で説明するよりも、実際の入力例と結果例を示したほうがわかりやすいとおもうので、以下に示します

assertEqual(List(1,2).sum, List(3,4).sum)
java.lang.AssertionError: 3 [scala.collection.immutable.List.apply[Int](1, 2).sum[Int](scala.math.Numeric.IntIsIntegral)] is not equals 7 [scala.collection.immutable.List.apply[Int](3, 4).sum[Int](scala.math.Numeric.IntIsIntegral)]

Scalaのマクロは、現状では少しフェーズが進んだ状態の抽象構文木しかとれない*4 *5ので、ソースコードの見た目そのままではなく、implicitが解決され"scala.math.Numeric.IntIsIntegral"が渡されてる状態が表示されていますが、まぁそれは仕方ないので気にしないでください。重要なのはエラーメッセージに

  • List(1,2).sumの計算結果である3と、
  • List(1,2).sumというソースコードの情報をある程度そのまま保った scala.collection.immutable.List.apply[Int](1, 2).sum[Int](scala.math.Numeric.IntIsIntegral) という表示

の両方がある点です。コンパイルした後の実行時には、「assertEqualの第一引数が List(1,2).sum というコードだった」という情報は取得のしようがありません。(コンパイラプラグイン除く)
これができるからこそのマクロです。

また、このマクロの副次的な効果(?)として、エラーメッセージは、エラーの際しか生成しない、かつエラメッセージを返すFunction0さえも生成しないので、効率がよくなる、というものもあります。
むしろ、こういった効率のほうを主目的としてマクロ書く場合もありえます。(それを目的として、この前ライブラリ作った http://d.hatena.ne.jp/xuwei/20140816/1408217977 )



さて、こういったマクロをどうやって書くのか?ですが、全部一から説明するのは面倒というか長くなり過ぎて今回説明したい部分にたどり着かないので、いきなり実装例を示します

scalaVersion := "2.11.2"

libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
import scala.reflect.macros.blackbox.Context
import scala.language.experimental.macros

object Util {
  def assertEqual(a: Any, b: Any): Unit = macro assertEqualImpl

  def assertEqualImpl(c: Context)(a: c.Expr[Any], b: c.Expr[Any]): c.Expr[Unit] = {
    import c.universe._
    val codeA = showCode(a.tree)
    val codeB = showCode(b.tree)
    val tree = q"""
    if($a != $b){
      val message = $a + " [" + $codeA + "] is not equals " + $b + " [" + $codeB + "]"
      throw new AssertionError(message)
    }
    """
    c.Expr(tree)
  }
}

先ほども書きましたが、Scala2.11前提で書いてるので、Scala2.10では動きません。
さて、これで確かに先ほどの assertEqual(List(1,2).sum, List(3,4).sum) ならば、一応動きます。


しかし、上記の実装例にはいくつか問題が潜んでいます。その問題と解決策が、今回説明したい部分です。つまり、上記のコードをみて、すぐに問題点がわかった人はこの先読まなくていいです。


問題点を把握するためにマクロをデバックするには、生成された抽象構文木をprintしてみるのがわかりやすいとおもいます。コンパイルオプションも一応ある?ようですが、おそらく個別にメソッド毎にprintはできない(?)ですし。つまりマクロの実装の中のtreeという変数を println( showCode(tree) ) とします。println(tree)でも大体近い表示になりますが、println( showCode(tree) )のほうが、記号メソッドなどがデコード前の元のソースコードに近い状態で表示されるなど細かな違いがあるので、showCode(tree)のほうがいいです。さて、println( showCode(tree) )すると以下のようになります。

if (scala.collection.immutable.List.apply[Int](1, 2).sum[Int](scala.math.Numeric.IntIsIntegral).!=(scala.collection.immutable.List.apply[Int](3, 4).sum[Int](scala.math.Numeric.IntIsIntegral)))
  {
    val message = scala.collection.immutable.List.apply[Int](1, 2).sum[Int](scala.math.Numeric.IntIsIntegral).+(" [").+("scala.collection.immutable.List.apply[Int](1, 2).sum[Int](scala.math.Numeric.IntIsIntegral)").+("] is not equals ").+(scala.collection.immutable.List.apply[Int](3, 4).sum[Int](scala.math.Numeric.IntIsIntegral)).+(" [").+("scala.collection.immutable.List.apply[Int](3, 4).sum[Int](scala.math.Numeric.IntIsIntegral)").+("]");
    throw new AssertionError(message)
  }
else
  ()

横に長くて見づらいですが、頑張ってください。さて、上記のtreeをprintしたものを見れば問題に気づくのではないでしょうか?
ここでの問題とは、タイトルに書いた「評価タイミング、評価回数」です。

この実装では、List(1,2).sum と List(3, 4).sumが、それぞれ2回評価されます。マクロに慣れていない人は、このあたりで少し混乱してくるとおもいますが、
マクロは「コンパイル時に」「抽象構文木を受けとって」「その抽象構文木を変形して返します」。


その「抽象構文木を受けとって」の部分ですが、あくまでコンパイル時には、 List(1,2).sum という構文木そのものであり、 List(1,2).sum を計算した結果ではありません。


なので、よくも悪くも、その同じ構文木をそのまま複数箇所に埋め込むことができます。今回は2箇所に埋め込んでしまってますね?
この構文木の埋め込みが意図したことならいいですが、今回のassertEqualマクロに関しては必要ないはずです。List(1, 2).sum程度ならいいですが、これがとても重いデータベースへのアクセスコードだったり、計算のたびに結果が変わるコード、もしくは外部の状態を変更する(たとえばデータベース更新)ものだったら大変なことになりますね?


具体的には、上記の実装で

assertEqual({println("side effect!"); 1}, 2)

という呼び方をすると、"side effect!" は、2回printされます。



というわけで、この問題を修正すると以下のようになります。

def assertEqualImpl(c: Context)(a: c.Expr[Any], b: c.Expr[Any]): c.Expr[Unit] = {
  import c.universe._
  val codeA = showCode(a.tree)
  val codeB = showCode(b.tree)
  val tree = q"""
  val c = $a
  val d = $b
  if(c != d){
    val message = c + " [" + $codeA + "] is not equals " + d + " [" + $codeB + "]"
    throw new AssertionError(message)
  }
  """
  c.Expr(tree)
}


さて、次は別の例で別の問題を説明します。以前がくぞーさんとはなしして作ったOptionに関するマクロをちょっと変えたものを例にして説明します。Optionに関するものとは、マクロ使わないで書くと以下の様なものでした

implicit class OptionOps(val bool: Boolean) extends AnyVal {
  def ??[A](f: => A): Option[A] = {
    if(bool) Some(f)
    else None
  }
}

true ?? 1 だと Option(1) になり、 false ?? 1 だと None になる、というだけのものです。これも途中の説明は省略して、出来上がったものがこちらになります

https://gist.github.com/xuwei-k/b302edb5f14a5457a58a

さて、少し例がわざとらしいですが、これを何故か Future[ Option[A] ] 型で返したくなった、としましょう。単純にFutureで包むように変えれば動くはずですね?実際以下の様に変えるだけで、大抵の場合動きます

val tree = q"""
  if(${c.prefix}.bool){
    concurrent.Future(Option($a))(concurrent.ExecutionContext.global)
  }else{
    concurrent.Future.successful(None)
  }
"""

さて上記のコードにも問題があります。今度はマクロの健全性(英語だと hygienic というらしい)の話です。(hygienic=健全なのか?の言葉の使い方や訳については、kmizuさんからのコメントもらったのでそちらも読みましょう)
これは、呼び出し側のimport文の如何によってコンパイル通るかどうか?が変わってしまいます。例えば以下のようなエラーを目撃することになるかもしれません

[error] Sample.scala:12: object ExecutionContext is not a member of package scalaz.concurrent
[error]     true ?? 1
[error]          ^
[error] Sample.scala:17: object java.util.concurrent.Future is not a value
[error]     true ?? 1
[error]          ^

それぞれ、"import scalaz._" があった場合(なおかつscalaz-concurrentが依存に入っていた場合)、"import java.util._" があった場合です。
大事なのことなのでもう一度言いますが、あくまでそのimportは「呼び出し側」に追加しただけで、マクロのコードは変えなくても、コンパイル通るかどうか?が変わってしまいます。これがマクロの厄介なところです。
当たり前すぎることですが、普通のScalaのメソッドでは、そのメソッド自体がコンパイル通ったならば、どんな呼び出し方をしても「呼び出し側のimportのなどの影響を受ける」 なんてことはありませんね?*6


しかし、Scalaのdefマクロは、その名の通りdefで定義され、使う側はほぼ普通のメソッドの感覚で使えるのですが、defマクロで返された抽象構文木がどの状態の抽象構文木なのか、それはその後どのように評価されるのか?を把握していないと、上記のような出来の悪いマクロにもし遭遇してしまった場合に、理解不能に陥るでしょう。簡単に言うと、defマクロで返されるのは、あくまで途中の抽象構文木であって、その後その抽象構文木内のシンボル(パッケージ名や変数名)の解決や、型検査、implicitの解決などは、呼び出し側のコンテキストの影響を受けます。


やっかいなのは、こういった衝突を起こしてしまった場合に、マクロで生成した抽象構文木には対応するソースコードとその行番号は存在しない*7ので、マクロを呼び出している部分でエラーが表示され、慣れていないとそのエラーみても「何言ってんだこいつ」としかならないという点です。



よって先ほどのものは、以下のように "scala.concurrent.Future"もしくは"_root_.scala.concurrent.Future"などと書いておかなくてはいけません。
*8

val tree = q"""
  if(${c.prefix}.bool){
    scala.concurrent.Future(Option($a))(scala.concurrent.ExecutionContext.global)
  }else{
    scala.concurrent.Future.successful(None)
  }
"""

上記のものは、「単にScalaの相対importが悪いだけでは」と思うかもしれません。ある意味そうなんですが、しかし自分もあまり詳しくないですが「マクロにおける健全性」とはもっと広い範囲のものだろうし、それをある意味悪用?した、swapなどを作ることができます。

// マクロ定義側
object Util {
  def swap[A](a: A, b: A): Unit = macro Util.swapImpl[A]

  def swapImpl[A](c: Context)(a: c.Tree, b: c.Tree): c.Tree = {
    import c.universe._
    q"""
      val tmp = $a; $a = $b; $b = tmp;
    """
  }
}
// マクロ呼び出し側
var x = 1
var y = 2
swap(x, y)
println("x = " + x)
println("y = " + y)
// 実行結果
x = 2
y = 1

このように、Scalaのマクロは健全ではないので、良くも悪くもシンボルを捕捉してしまうことが可能です。Common Lispではそれらを防ぐために、gensymというユニークなことが保証されたシンボルを生成するものが用意されています。Scalaでも同様にあります。なので、今まで書いたなかで、適当にcとかdとかtmpとかの変数名を使いましたが、そういったものも衝突の危険性をはらんでいるので、基本的には全部そういうシンボルは自動生成するべきです。*9 たとえば上記のswapのtmpを自動生成したものを使うように書き換えると以下です。

def swapImpl[A](c: Context)(a: c.Tree, b: c.Tree): c.Tree = {
  import c.universe._
  val tmp = TermName(c.freshName())
  q"""
    val $tmp = $a; $a = $b; $b = $tmp;
  """
}


さて、健全でないことにより、わざとシンボルを捕捉可能なことを悪用するテクニックを使ったマクロのことをアナフォリックマクロというらしい?です。(このあたりそれほど詳しくないので、どこからをアナフォリックマクロというのかあやしい・・・) そういうのを知りたい人は、On Lisp や Let Over Lambda LET OVER LAMBDA Edition 1.0 を読むといいでしょう。


ちなみに、Scalaの(少なくとも普通のdefの)マクロでは、On Lispに載っているようなCommon Lispのアナフォリックマクロは、全部真似できないようです。*10なぜかというと、defマクロに渡される時点で、最低限のScalaとしてのシンタックスが検査されるため
「現時点では存在しないマクロで生成されるはずのシンボルを使ったコードを書いてマクロに渡す」
をやろうとしても、マクロに渡す前のチェックでエラーになってしまいます。このようにScalaでのマクロは、どの時点でどのようなチェックがされるのか?を把握するまでは、そういう罠にハマった場合にかなり混乱すると思います。Lispよりも型検査などが多くなる分、考えなければいけない事もある意味多いかもしれません?


さて、このあたりのScalaのマクロの健全性は、将来的に変わるか、健全にマクロを書けるようにする機能を入れる構想もある程度あるらしい?のですが、直近ではなくある程度遠い将来?でしょうし、自分も追ってないのでよくわかりません。以下のページ読んだり
http://docs.scala-lang.org/overviews/quasiquotes/hygiene.html
scalametaで検索してみてください。


以上、結構長くなったけど、Scalaのマクロの基礎(?)の話でした

*1:マクロはScala2.10から入っている。2.10の安定版が最初にでたのは2013年1月

*2:Lispのマクロのことが多く書いてある有名な本

*3:Anyではなく、ジェネリックにしたり、型を制限するとかも考えられますが、そこは今回説明するマクロで説明したい部分とあまり関係ないので、Anyで説明していきます

*4:具体的には、typerというフェーズのタイミング?

*5:将来的にscalametaというプロジェクトでは、もっと初期段階のソースにちかいすべての情報をとれるようになる、という話もある

*6:implicitパラメーターがあれば、影響受けると言えば受けますが、それもあくまで呼び出し側での話であり、今回は、"マクロの内部がエラーになってしまう" という話なのでそれとも違う

*7:手作業でマクロ呼び出し側で行番号をつけることは一応可能だが・・・

*8:余談ですが、scalaや_root_というパッケージ名や変数名がScalaでは作れてしまうので、それでも完全な解決にはならないのだが・・・。流石にそれはそういう変数を作るほうが悪い、でいいのだろうか・・・

*9:実際にトラブル発生した例 https://github.com/cb372/scalacache/commit/d2ca4205e

*10: https://groups.google.com/d/topic/scala-user/o9R_VDSuGAM/discussion