Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Scala でも Python の decorator が使いたい?

More than 3 years have passed since last update.

同期から『Scala で Python の decorator を使ってメソッドの入出力をロギングしたい』と圧力を受けたので、Scala のマクロアノテーション機能を使って decorator を作れるライブラリを試作してみました。

Python のデコレータと Scala のアノテーション

Python はあまり書かないので詳しいことは知りませんが、Scala (あるいは Java) のアノテーションによく似た見た目の decorator1 という機能があるそうです。この機能を使うと関数やメソッドをラップして加工することができます。

以下の例は、python.org から持ってきたものです。この例では、関数 p1, p2 に対して decorator として @require_int がついていて、引数が int であるかを確認する機能を付加しています。この @require_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

まず作ったのは以下です。

decorator.scala
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 を作ってみました。やはりアノテーションの名前を自由に決められた方がそれっぽいですよね。

logger.scala
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をインスタンスメソッドとして以下のように定義した方が素直に見えると思います。

logger.scala
class logger extends namedDecorator {
  def apply[X, R](f: X => R) = (x: X) => {
    println(x)
    val r = f(x)
    println(r)
    r
  }
}

しかし、このアプローチは、今のところまだ手元でうまくいっていません。解決したらまた後日報告するかもしれません。

一応コメントすると、Scala マクロ、特にマクロパラダイスの闇は深いので、ハマったら正面から戦うのは諦めてさっさと撤退したほうが身のためです。。

今回手抜きした点

今回、単なる試作品なので、いくつかの点で手抜きしています。例えば、

  1. パラメータ多相はどうするのか
  2. 引数リストが複数あるメソッドはどうするのか
  3. 名前呼びや暗黙の引数があるメソッドはどうするのか

などです。暇があったら挑戦してみるといいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away