同期から『Scala で Python の decorator を使ってメソッドの入出力をロギングしたい』と圧力を受けたので、Scala のマクロアノテーション機能を使って decorator を作れるライブラリを試作してみました。
Python のデコレータと Scala のアノテーション
Python はあまり書かないので詳しいことは知りませんが、Scala (あるいは Java) のアノテーションによく似た見た目の decorator1 という機能があるそうです。この機能を使うと関数やメソッドをラップして加工することができます。
以下の例は、python.org から持ってきたものです。この例では、関数 p1
, p2
に対して decorator として @require_int
がついていて、引数が int
であるかを確認する機能を付加しています。この @require_int
の実体は上で定義された関数 require_int
です。この関数は関数を受け取って関数を返す関数になっています。
def require_int (func):
def wrapper (arg):
assert isinstance(arg, int)
return func(arg)
return wrapper
@require_int
def p1 (arg):
print arg
@require_int
def p2(arg):
print arg*2
一方、Scala のアノテーションは、見た目がよく似ているものの、かなり趣が異なります。具体的な処理を示す decorator と異なり、アノテーションは付加情報を示すオブジェクトであり、ホスト言語上では具体的な意味を持ちません。アノテーションの意味は、別に作られたアノテーションプロセッサによって与えられるものです。
この記事では、そういったアノテーションプロセッサの1つで、アノテーションにマクロとしての意味を持たせる Macro Paradise plugin を用いて decorator のようなものを作ることを試みました。ところで、毎回アドホックにマクロを書くアプローチは非常に単純であり、これは以前の記事2で紹介したように既に作りました。そこで今回はより汎用的なものを目指して、decorator 開発者はマクロを書かなくて良いようにすることを要件としました。
Decorator マクロ V1
まず作ったのは以下です。
def logger[X, R](f: X => R) = (x: X) => {
println(x)
val r = f(x)
println(r)
r
}
@decorator(logger)
def hello(a: Int) = a
このバージョンでは、Python のデコレータとは見た目が変わってしまいますが、汎用のマクロアノテーション @decorator
を作りました。引数に関数をとって関数を返す関数をとることで、実際の加工処理を指定することもできます。例では、ログング処理を追加するメソッド logger
を渡しています。なお、これは実際には以下のように展開されます。
def hello(a: Int) = (logger)(hello$1 _)(a)
def hello$1(a: Int) = a
他の使用例や実装の詳細は下のリンクにあります。
Decorator マクロ V2
V1 ではあまり満足できなかったので V2 を作ってみました。やはりアノテーションの名前を自由に決められた方がそれっぽいですよね。
class logger extends namedDecorator
object logger {
def apply[X, R](f: X => R) = (x: X) => {
println(x)
val r = f(x)
println(r)
r
}
}
@logger
def hello(a: Int) = a
確かに見た目がそれっぽくなりました。仕組みとしては、namedDecorator
trait を mix-in すると apply
という名前でコンパニオンオブジェクトに定義した変換メソッドが挿入されるようになっています。Trait の内部にマクロを隠すことで、decorator 開発者がマクロを触ることはやはり回避できています。ボイラープレートが少し多めなので、変換メソッドを書いたら class と object が生成されるようなマクロアノテーションを書いてもいいかもしれません。
他の使用例や実装の詳細は下のリンクにあります。
Decorator マクロ V0 (失敗)
普通に考えるとコンパニオンオブジェクトを使わずに、apply
をインスタンスメソッドとして以下のように定義した方が素直に見えると思います。
class logger extends namedDecorator {
def apply[X, R](f: X => R) = (x: X) => {
println(x)
val r = f(x)
println(r)
r
}
}
しかし、このアプローチは、今のところまだ手元でうまくいっていません。解決したらまた後日報告するかもしれません。
一応コメントすると、Scala マクロ、特にマクロパラダイスの闇は深いので、ハマったら正面から戦うのは諦めてさっさと撤退したほうが身のためです。。
今回手抜きした点
今回、単なる試作品なので、いくつかの点で手抜きしています。例えば、
- パラメータ多相はどうするのか
- 引数リストが複数あるメソッドはどうするのか
- 名前呼びや暗黙の引数があるメソッドはどうするのか
などです。暇があったら挑戦してみるといいと思います。