macros

準クォート

MACRO PARADISE

Eugene Burmako 著
Eugene Yokota 訳

準クォート (quasiquote) は 2.11.0-M4 以降の Scala 2.11 のマイルストーン版から含まれる機能だ。Scala 2.10 からもマクロパラダイスプラグインを使うことによって利用可能だ。詳細はマクロパラダイスページの説明にしたがってコンパイラプラグインをダウンロードしてほしい。

直観

例として、あるクラスかオブジェクトを受け取り、その全てのメソッドを future でラッピングした非同期版の複製を作る async というマクロアノテーションを考える。

@async
class D {
  def x = 2
  // def asyncX = future { 2 }
}

val d = new D
d.asyncX onComplete {
  case Success(x) => println(x)
  case Failure(_) => println("failed")
}

そのようなマクロの実装は以下のコードの抜粋のようにできる。この取得、分解、生成コードでラッピング、再構築という流れはマクロ作者にとっては見慣れたものだ。

case ClassDef(_, _, _, Template(_, _, defs)) =>
  val defs1 = defs collect {
    case DefDef(mods, name, tparams, vparamss, tpt, body) =>
      val tpt1 = if (tpt.isEmpty) tpt else AppliedTypeTree(
        Ident(newTermName("Future")), List(tpt))
      val body1 = Apply(
        Ident(newTermName("future")), List(body))
      val name1 = newTermName("async" + name.capitalize)
      DefDef(mods, name1, tparams, vparamss, tpt1, body1)
  }
  Template(Nil, emptyValDef, defs ::: defs1)

しかし、ベテランのマクロ作者でもこのコードは、かなりシンプルであることは確かだが、例えば、AppliedTypeTreeApply の違いなどコードの内部表現の詳細に理解していることを必要とした必要以上に冗長なものであることを認めるだろう。準クォートはパラメータ化された Scala のコードを Scala を使って表現できるドメイン特化言語を提供する:

val q"class $name extends Liftable { ..$body }" = tree

val newdefs = body collect {
  case q"def $name[..$tparams](...$vparamss): $tpt = $body" =>
    val tpt1 = if (tpt.isEmpty) tpt else tq"Future[$tpt]"
    val name1 = newTermName("async" + name.capitalize)
    q"def $name1[..$tparams](...$vparamss): $tpt1 = future { $body }"
}

q"class $name extends AnyRef { ..${body ++ newdefs} }"

現行の準クォートは SI-6842 のため、上記のようには簡潔に書くことができない。多くのキャストを適用して使えるようになる。

詳細

準クォートは scala.reflect.api.Universe cake の一部として実装されているため、マクロから準クォートを使うには import c.universe._ とするだけでいい。公開されている API は qtqcq、そして pq 文字列補間子を提供し (値と型の準クォートに対応する)、構築と分解の両方をサポートする。つまり、普通のコードとパターンケースの左辺値において使うことができる。

補間子対象構築分解
q値構文木q"future{ $body }"case q"future{ $body }" =>
tq型構文木tq"Future[$t]"case tq"Future[$t]" =>
cqケースcq"x => x"case cq"$pat => ${_}" =>
pqパターンpq"xs @ (hd :: tl)"case pq"$id @ ${_}" =>

普通の文字列補間子と違い、準クォートは単独の構文木、構文木のリスト、構文木のリストのリストの挿入または抽出を区別するために複数のスプライシングの方法をサポートしている。スプライス対象とスプライス演算子の基数のミスマッチはコンパイル時のエラーとなる。

scala> val name = TypeName("C")
name: reflect.runtime.universe.TypeName = C

scala> val q"class $name1" = q"class $name"
name1: reflect.runtime.universe.Name = C

scala> val args = List(Literal(Constant(2)))
args: List[reflect.runtime.universe.Literal] = List(2)

scala> val q"foo(..$args1)" = q"foo(..$args)"
args1: List[reflect.runtime.universe.Tree] = List(2)

scala> val argss = List(List(Literal(Constant(2))), List(Literal(Constant(3))))
argss: List[List[reflect.runtime.universe.Literal]] = List(List(2), List(3))

scala> val q"foo(...$argss1)" = q"foo(...$argss)"
argss1: List[List[reflect.runtime.universe.Tree]] = List(List(2), List(3))

コツとトリック

Liftable

非構文木のスプライシングを簡易化するために、準クォートは Lifttable 型クラスを提供して、値がスプライスされたときにどのように構文木に変換されるかを定義する。プリミティブ型と文字列を Literal(Constant(...)) にラッピングする Liftable インスタンスは提供されている。簡単なケースクラスやリストのための独自のインスタンスを定義することをお勧めする。

trait Liftable[T] {
  def apply(universe: api.Universe, value: T): universe.Tree
}
blog comments powered by Disqus