groovy-macro-methodsについて(前編)
はじめに
これはG*アドベントカレンダーの第14日目の記事です。
今回は、groovy-macro-methodsをご紹介します。
なお、本記事は半年くらい前のワークショップで発表する予定だったネタですが、時間の都合上発表できなかったので、今回その時のネタを使い回すことにしました。
groovy-macro-methodsとは
簡単に言いますと、groovy-macro-methodsは、AST(Abstract Syntax Tree : 抽象構文木)を引数に受け取り変換したASTを返すメソッドを、マクロ(コンパイル時にASTを変換する機能)としてサポートするためのライブラリです。
詳細は、次のspeakerdeckを参照してください。
groovy-macro-methodsの例
例えば、
def nullObject = null assert null == nullObject.hashCode()
といったコードがある場合、NullPointerExceptionで落ちます。これを、
def nullObject = null assert null == safe(nullObject.hashCode())
という風にsafeマクロで「コンパイル時にASTを変換」して、NullPointerExceptionで落ちないようにしたいと思います。つまり、「nullObject.hashCode()」といったメソッド呼び出しのASTを引数に受け取り、
def nullObject = null assert null == (nullObject != null ? nullObject.hashCode() : null)
といった三項演算子のASTに変換してコンパイル時に置き換える、というマクロを定義します。この場合のsafeマクロの定義は次のようになります。
package foo.bar; import org.codehaus.groovy.ast.expr.*; import static org.codehaus.groovy.ast.tools.GeneralUtils.*; import ru.trylogic.groovy.macro.runtime.*; public class TestMacroMethods { @Macro public static Expression safe(MacroContext macroContext, MethodCallExpression callExpression) { return ternaryX(notNullX(callExpression.getObjectExpression()), callExpression, constX(null)); } }
引数に「メソッド呼び出しの結果」ではなく「メソッド呼び出しのAST」を受け取り、三項演算子のASTに変換した結果(のAST)を戻り値として返します。 notNullX()が「引数がnullかどうかを判定するAST」を生成するメソッド(三項演算子の「nullObject != null」の部分)、constX()が「引数を定数として表すAST」を生成するメソッド、ternaryX()が「三項演算子のAST」を生成するメソッド、となります。
groovy-macro-methodsを使用したマクロの作り方
groovy-macro-methodsを使用してマクロを作る際、次の点に気をつけてください。
- public staticメソッドを用意する。
- メソッドに@Macroを付ける。
- メソッドの第一引数はMacroContextとする。
- メソッドの第二引数以降は変換したいASTのクラスとする。
- 戻り値はExpressionかExpressionの派生クラスとする。
- メソッド定義とは別に、拡張モジュールの定義を行う。
public staticメソッドを用意する。
マクロは、public staticのメソッドとして定義します。public staticでないとマクロとして認識してくれません。
メソッドに@Macroを付ける。
このメソッドがマクロであると宣言します。
メソッドの第一引数はMacroContextとする。
メソッドの第一引数はMacroContext型の値です。MacroContext自体は、次のクラスのオブジェクトを持ちます。
- CompilationUnit
- SourceUnit
- マクロ自体のMethodCallExpression
先の例のように、必ずしも使用しないといけないわけではありません。
メソッドの第二引数以降は変換したいASTのクラスとする。
メソッドの第二引数以降は変換したいASTのクラスを指定します。 ただし、指定できるASTのクラスは、Expression(式)かその派生クラスのみとなります。なので、Statement(文)やその派生クラスをマクロに渡して変換することはできません。 これをなんとかしたい場合、ClosureExpression(クロージャ式)を引数とするマクロを定義し、ClosureExpression内のBlockStatement内のStatementを変換、といったことならできます。
戻り値はExpressionかExpressionの派生クラスとする。
戻り値はExpression(式)かExpressionの派生クラスとなります。Statement(文)やその派生クラスを戻り値として返すことはできません。 これをなんとかしたい場合、Closureを定義してcallメソッドを呼び出す、つまりClosureExpression(クロージャ式)のMethodCallExpression(メソッド呼び出し)を戻り値として返すマクロを定義し、そのClosureExpressionの中でStatementやその派生クラスを定義する、といったことならできます。
メソッド定義とは別に、拡張モジュールの定義を行う。
メソッドを定義するだけではマクロとして機能せず、groovyの拡張モジュールの定義を行う必要があります。
ファイル「META-INF/services/org.codehaus.groovy.runtime.ExtensionModule」を作成し、extensionClassesにマクロの定義を持つクラスのFQCN(完全修飾クラス名)を指定します。例えば先のsafeマクロの拡張モジュールの定義は次のとおりです。
moduleName=macro-module moduleVersion=1.0 extensionClasses=foo.bar.TestMacroMethods
最終的には
マクロを使用した場合、最終的には、
def nullObject = null assert null == safe(nullObject.hashCode())
というコードは、
def nullObject = null assert null == MacroStub.INSTANCE.macroMethod (nullObject != null ? nullObject.hashCode() : null)
というコードにコンパイル時に変換され実行されます。macroMethodメソッド自体は、
public enum MacroStub { INSTANCE; public <T> T macroMethod(T obj) { return obj; } }
のように引数を返すだけのメソッド呼び出しとなっています。
マクロの使い所
普通のメソッド呼び出しで事足りるのであれば、マクロを使う必要はないと思います。普通のメソッド呼び出しでは実現できない場合にマクロ(= AST変換)が使えないか検討してみてください。例えば、DSL(Domain Specification Language)、糖衣構文(シンタックスシュガー)、等です。
マクロ開発の問題点
マクロを開発する上で問題となることと言えば、マクロがどういうASTを返しているか、想定したASTを返しているか、といったことがわかりづらい、ということでしょう。
これを解決するために、GroovyConsoleを使用します。GroovyConsoleで「Inspect Ast」を実行すると、「Semantic Analysis」以降であれば、マクロを呼び出した際のAST変換後のコードを見ることができます。
後編に続く
さて、前編はここまで、groovy-macro-methodsとは何か、groovy-macro-methodsを使ったマクロの作り方をご紹介しました。
後編は、groovy-macro-methodsを使った「あまり実用的ではないのですが、サンプルとしてはまぁ何とか...」というマクロをいくつか作ってみましたので、それをご紹介することで、groovy-macro-methodsでどんなことができるかご紹介したいと思います。