倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Scala のマクロで簡単なリフレクティブなアクセッサを書いてみる。

ちょっと Scala のマクロの練習に、任意オブジェクトに対して文字列で指定した名前のプロパティ(Scala でもプロパティっていうんだっけ?)にアクセスするコードを書いてみました。 プロパティ値を取得するものすごく簡単なものだけど。 使い方は以下のようなもの:

object Main extends App{
  // Person クラスは name, age という2つのプロパティを持つ。
  case class Person(name:String, age:Int)

  val me = Person("waman", 100)

  // Macro コンパニオンオブジェクトの getProperty() でプロパティ・アクセス
  println(Macro.getProperty(me, "name"))  // 「waman」と表示
  println(Macro.getProperty(me, "age"))  // 「100」と表示
}

commons-beanutils だかなんだかにこんなユーティリティ・クラスあったような気がするけど(BeanUtils ってな名前で)。

メソッド宣言

まずは実装をマクロに投げるメソッド宣言。 マクロの実装は getPropertyImpl に書きます:

object Macro{
  def getProperty(obj: Any, name: String): Any = macro getPropertyImpl
}

macro」キーワードでマクロの実装を指定します。 必要なら*1

import scala.language.experimental.macros

をつけておきましょう。 マクロ実装 getPropertyImpl の宣言は getProperty() の宣言を機械的に書き換えたもの:

import scala.reflect.macros.blackbox.Context

object Macro{
  def getProperty(obj: Any, name: String): Any = macro getPropertyImpl

  /** マクロの実装 */
  def getPropertyImpl(c: Context)(obj: c.Tree, name: c.Tree) = {
    import c.universe._
    ...
  }
}

1つ目のパラメータリストには (black/white) Context、2つ目のパラメータリストには getProperty() の引数に対応するパラメータ(ただし、型は c.Tree もしくは c.Expr[...])を宣言します。 c.Tree 型を宣言した場合は、返り値を省略できるもよう(c.Expr[...] の場合はダメなのかな?)。

import c.universe._」は Scala のリフレクション関連で使う、コンテキスト依存な型やら関数やらなんやらかやらをインポートしてるみたいだけど、まぁ、定型文と思っておけばいいようですな。

で、このマクロ実装で実行したいコードの AST (抽象構文木)を返せば OK。 ちなみに、Scala 2.10 では返り値を c.Expr(...) でラップしないといけなかったようですが、Scala 2.11 では必要なら自動でやってくれるとのこと。

引数の AST、返り値の AST

さて、次は getPropertyImpl() の実装をするんですが、それには AST をイジるコードを書く必要があります。 ただ、ScalaLispClojure と違ってマクロでイジる AST が通常の Scala コードと見た目がかけ離れているので、いきなりコードを書くのはハードルが高い気がします(と知ったかぶり?して書いてみた)。 ということで、まずは引数の AST(c.Tree オブジェクト)や、マクロ実装が返したい AST がどんなものかを具体的に見てみましょう。 AST の具体的な構成は

  1. import scala.reflect.runtime.universe._」を追加
  2. showRaw(《AST》) によって AST を表す文字列を取得

によって見ることができます。 で、最初のサンプル・コードで Macro.getPropertyImpl() を呼び出してみると、2つの引数 obj, name の AST はそれぞれ

obj Select(This(TypeName("Main")), TermName("me"))
name Literal(Constant("age"))

となってることが分かります。 メモのために、一応 AST を表示させるコードを載せておきます:

import scala.reflect.macros.blackbox.Context
import scala.reflect.runtime.universe._

object Macro{
  def getPropertyImpl(c: Context)(obj: c.Tree, name: c.Tree) = {
    import c.universe._

    println(showRaw(obj))  // Select(This(TypeName("Main")), TermName("me"))
    println(showRaw(name))  // Literal(Constant("age"))
    ...
  }
}

obj オブジェクトは「Main」コンパニオン・オブジェクトの「me」変数(の値)ってことですね。 Select や This やらがなんのことやら、という感じ。 一方、マクロ実装が返したい AST は「Macro.getProperty(obj, "age")」と呼び出されたときに「obj.age」というプロパティ・アクセスをするコードに対応するものです:

obj.name Select(Ident(TermName("obj")), TermName("age"))

これは準クォート (quasiquote) を使えば確認できます:

  println(showRaw(q"obj.age"))  // Select(Ident(TermName("obj")), TermName("age"))

ここでの obj は引数の obj とは異なります(別に getPropertyImpl() 内で実行する必要もないし)。 これで、どんな AST が渡されて、どんな AST を返せばいいのかがある程度分かったかと思います。

マクロ実装

では、引数の AST からプロパティ・アクセスする AST を構築しましょう。 まぁ、プロパティ・アクセスの AST は簡単なので、以下のような感じにすればいいんだろうという察しはつきますね:

def getPropertyImpl(c: Context)(obj: c.Tree, name: c.Tree) = {
  import c.universe._
  Select(
    obj,
    TermName(《プロパティ名》)
  )
}

Select や TermName は c.universe._ によってインポートされてるのかな? まぁ、普通に使えます。 準クォートを使うなら

def getPropertyImpl(c: Context)(obj: c.Tree, name: c.Tree) = {
  import c.universe._
  q"$obj.《プロパティ名》"
}

で、後は《プロパティ名》の部分にプロパティ名を表す String オブジェクト を渡せばいいんですが、getPropertyImpl() の引数の name は AST (c.Tree オブジェクト、)なので、これを直接渡すと怒られます。 "age" が要求されているところに Literal(Constant("age")) を渡してるってことね。 ってことで、AST から情報を抜き出す必要がありますが、これにはパターン・マッチングを使うと便利。 具体的にどうするかというと

val Literal(Constant(propName: String)) = name

という風に name の AST の構造にそったパターンによって変数を宣言する、みたいなことをしてやります。 おぉ、関数型プログラミング! ここでは String 型の propName という変数を宣言しています。 これを使うとマクロ実装は次のように書けます:

  val Literal(Constant(propName:String) = name)
  val propTName = TermName(propName)
  q"$obj.$propTName"

  // 準クォートを使わないなら、最後2行の代わりに以下を。
  // Select(
  //    obj,
  //    TermName(propName)
  //  )

アンクォート(「$」による展開)して《プロパティ名》にするために一度 TermName を作ってるのが少々回りくどいきもするけど、まぁそんなもんなんでしょう。 とりあえず、これで完了。

コードまとめ

import scala.reflect.macros.blackbox.Context

object Macro{

  def getProperty(obj:Any, name:String):Any = macro getPropertyImpl

  def getPropertyImpl(c: Context)(obj: c.Tree, name: c.Tree) = {
    import c.universe._

    val Literal(Constant(propName: String)) = name
    val propTName = TermName(propName)
    q"$obj.$propTName"
  }
}

この実装を使ってを最初のサンプルを実行すると、確かに期待したように動きます。 なんか、意外と簡単にまとめられた感じがしますがどうでしょう。

追記
後で試そうと思って忘れてたんですが、Macro.getProperty() の第2引数が文字列リテラルでない場合はうまくいかないですね。 困った、困った。

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

*1:scalac のコンパイル・オプションに「-language:experimental.macros」をつけてれば必要なし。