5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-02-08

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

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

モチベーション

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

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

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

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型を引数に取るクラスを定義します。

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オブジェクトを受け取ってインポートを書くのは面倒なので以下のような感じに定義することになると思います。

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を適宜付け替えつつ複製
  
  ...
}

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

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という有名ライブラリ4ですと複数のトレイトに分割して組み上げるcake patternの一種を採用しています。別クラスにしてしまうと上記の問題が発生してしまうので、同じクラスとして1つに組み上げてしまうわけですね。

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

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

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

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

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

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

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の部分です。オリジナルと違ってあくまでミックスインではなくコンストラクタ経由での注入なのでこのような違いが出ているのだと思います。67

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

Inline/meta系のマクロでは…?

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

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

  1. https://github.com/hiroshi-cl/InverseFramework

  2. http://qiita.com/pab_tech/items/1c0bdbc8a61949891f1f

  3. http://qiita.com/tayama0324/items/7f87ee3672b15dd68016

  4. https://github.com/scala/async

  5. https://github.com/scala/async/blob/v0.9.6/src/main/scala/scala/async/internal/AnfTransform.scala#L12

  6. というわけなのでこれをミックスイン・インジェクションと呼ぶのはちょっと違うかなと思います

  7. セッター・インジェクションでも似たようなことをやってみてもいいのではないでしょうか

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?