田山さんのcake patternを使ったScalaマクロ・コンパイラプラグインのモジュール化

マクロ・コンパイラプラグインのモジュール化はパス依存型により結構面倒です。ベストプラクティスはよくわかりませんが、1年前にinverse macroをリファクタリング[^0]したときに田山さんのcake pattern[^1][^2]を参考にしたのでそれを紹介します。

**もっといいプラクティスがあれば随時情報募集中です。**

[^0]: https://github.com/hiroshi-cl/InverseFramework
[^1]: http://qiita.com/pab_tech/items/1c0bdbc8a61949891f1f
[^2]: http://qiita.com/tayama0324/items/7f87ee3672b15dd68016

## モチベーション

### 背景: マクロ・コンパイラプラグインの作り

マクロ・コンパイラプラグインのモジュール化の前に、Scalaのマクロ・コンパイラプラグインの開発について簡単に説明したいと思います。マクロ・コンパイラプラグインの開発においては、ある1つのオブジェクトを引数に取るようなクラス、言い換えるとDIにおけるコンストラクタインジェクションの形式になっているクラスを定義することが肝となります。

例えば、マクロでは以下のようにクラスを定義します。

```scala:macro
import scala.language.experimental.macros

class Bundle(val c: scala.reflect.macros.blackbox.Context) {
  import c.universe._

  def transformImpl(a: Tree): Tree = ... // マクロの本体
}
```

このように`Context`型の引数を注入される形となります。ここで、`Context`はマクロ展開の文脈に関する情報を持ったオブジェクトですが、それだけでなく様々なクラス定義を含んでいます。例えば、構文木を示す`Tree`クラスや型を示す`Type`クラスなどです。そのため、`import c.universe._`のようにインポートを行って使います。

コンパイラプラグインの場合も同様に以下のように`Global`型を引数に取るクラスを定義します。

```scala:compiler_plugin
class Plugin(override val global: scala.tools.nsc.Global)
  extends scala.tools.nsc.plugins.Plugin {
  import global._
  import analyzer._

  val Analyzer = new AnalyzerPlugin {...} // 一例

  override def init(options: List[String], error: (String) => Unit): Boolean = {
    addAnalyzerPlugin(Analyzer)
    true
  }
}
```

### 問題点

1回コンストラクタで受け取れば明示的に引き回さなくてもクラス内のメソッドで共有できるので、ある意味スッキリしたフレームワーク設計になっています。しかしながら、モジュール化を考えたときには、この設計には、パス依存型というとても面倒な問題を抱えることになります。例えば、symbolを考慮して構文木をコピーするメソッドなど複数のユーティリティメソッドを`Util`という別クラスに切り出すことを考えましょう。各メソッドごとに`Context`オブジェクトを受け取ってインポートを書くのは面倒なので以下のような感じに定義することになると思います。

```scala:Util.scala
import scala.language.experimental.macros

class Util(val c: scala.reflect.macros.blackbox.Context) {
  import c.universe._

  def copy(a: Tree, n: Int): Seq[Tree] = ... // symbolを適宜付け替えつつ複製
  
  ...
}
```

また使う側は以下のようにすると思います。

```scala:Bundle.scala
class Bundle(val c: scala.reflect.macros.blackbox.Context) {
  import c.universe._
  
  val util = new Util(c)

  def transformImpl(a: Tree): Tree = {
    ...
    val trees: Seq[Tree] = util.copy(tree, 2) // ???
    ...
  }
}
```

しかしこれは問題が起きます。`transformImpl`の中の`Tree`をもっと正確に書くと`c.universe.Tree`型で、`copy`の中では`util.c.universe.Tree`型で実は微妙に違いそうに見える型です。実際Scalaの型システムでは違うと言われてエラーになります。キャストしまくればなんとかなったりしますがかなり残念な気持ちになります。

**補足: 私にはパス依存型は難しすぎました。パス依存型のプロによるよりいい感じの解説待ってます。。**

### Scala Asyncの場合: cake pattern

他のマクロライブラリではどうしているのでしょうか？一般にどうといえるほど多くのマクロライブラリの中身を解析したわけではないのですが、Scala Asyncという有名ライブラリ[^3]ですと複数のトレイトに分割して組み上げるcake patternの一種を採用しています。別クラスにしてしまうと上記の問題が発生してしまうので、同じクラスとして1つに組み上げてしまうわけですね。

しかし惜しいことにScala Asyncではself type annotationが組み上げ後のクラスにfixされてしまっています。[^4] このままでは他のマクロに使いまわそうとしたとき問題が生じてしまうため、今回のように汎用ライブラリを切り出すという目的ではもう一捻りが必要です。

[^3]: https://github.com/scala/async
[^4]: https://github.com/scala/async/blob/v0.9.6/src/main/scala/scala/async/internal/AnfTransform.scala#L12

## 田山さんのcake patternを使った方法

今回は田山さんのcake patternを採用してみました。基本的にやることはScala Asyncと同様にtraitに分割するだけですが、トレイトの定義に一工夫入れます。以下マクロで説明しますが、コンパイラプラグインでもほぼ同じです。

まず、以下のように`UseContext`トレイトを定義します。オリジナルの田山さんのcake patternと違って、`Context`オブジェクトはインポートする必要があるので`def`ではダメですので注意してください。

```scala:UseContext.scala
trait UseContext {
  val c: scala.reflect.macros.blackbox.Context
}
```

パーツとして分けられた各トレイトではこれを継承します。これによって、パス依存型の問題に悩まされることも、組み上げ後のクラスを固定されることもなく`Util`を切り出すことができました。

```scala:Util.scala
trait Util extends UseContext {
  import c.universe._

  def copy(a: Tree, n: Int): Seq[Tree] = ... // symbolを適宜付け替えつつ複製
  
  ...
}
```

ところで、オリジナルの田山さんのcake patternと違って`MixInContext`は存在しません。Scalaのクラスパラメータに不慣れな人にはわかりにくいかと思いますが、クラスパラメータがそれに相当します。以下でいうと`val c: scala.reflect.macros.blackbox.Context`の部分です。オリジナルと違ってあくまでミックスインではなくコンストラクタ経由での注入なのでこのような違いが出ているのだと思います。[^5][^6]

```scala
class Bundle(val c: scala.reflect.macros.blackbox.Context)
  extends ... {}
```

[^5]: というわけなのでこれをミックスイン・インジェクションと呼ぶのはちょっと違うかなと思います
[^6]: セッター・インジェクションでも似たようなことをやってみてもいいのではないでしょうか


## Inline/meta系のマクロでは…？

そもそもなぜ現行のマクロでこんなに面倒になったのかというと、`Context`オブジェクトが`Tree`とか`Type`とかの定義を持っていてパス依存になるからでした。Scala.metaベースのinline/meta系マクロは、`scala.meta`が持つことになるようです。つまりファイルの先頭に`import scala.meta._`と書くだけでよいです。`Context`オブジェクトを引き回してインポートする必要はもはやありません。

冷静に考えてみると、構文木や型の定義が文脈依存というのはなんか設計が間違ってそうな気がします。今までの設計は何だったのか…というため息が出ますね。はー