ちょっと 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 をイジるコードを書く必要があります。 ただ、Scala は Lisp や Clojure と違ってマクロでイジる AST が通常の Scala コードと見た目がかけ離れているので、いきなりコードを書くのはハードルが高い気がします(と知ったかぶり?して書いてみた)。 ということで、まずは引数の AST(c.Tree オブジェクト)や、マクロ実装が返したい AST がどんなものかを具体的に見てみましょう。 AST の具体的な構成は- 「import scala.reflect.runtime.universe._」を追加
- 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引数が文字列リテラルでない場合はうまくいかないですね。 困った、困った。- 作者: Martin Odersky,Lex Spoon,Bill Venners,羽生田栄一,水島宏太,長尾高弘
- 出版社/メーカー: インプレスジャパン
- 発売日: 2011/09/27
- メディア: 単行本(ソフトカバー)
- 購入: 12人 クリック: 235回
- この商品を含むブログ (45件) を見る