LoginSignup
12

More than 5 years have passed since last update.

Scala.meta 1.0.0 記念: Macro Paradise 3.0.0-M{1,2,3} 紹介

Last updated at Posted at 2016-06-19

Scala.meta 1.0.0 がリリースされたみたいですね!それはさておきこの記事では、最近登場したMacro Paradise 3.0.0-M{1,2,3}の紹介します。

Macro Paradise 3.0.0-M{1,2,3} は SBT で以下のように書くことで使えます。

addCompilerPlugin("org.scalamacros" % "paradise" % "3.0.0-M3" cross CrossVersion.full)

Scala meta

Scala meta は、Scala コードを解析したり、自動生成したりする人のためのフレームワークです。アプリケーションとしてコードフォマッターの Scalafmt や、静的解析器の Codacy などがあるそうです。最終的には、コンパイラやマクロなどもサポートする予定のようですが、1.0.0 では Scala の構文木を表現するためのクラス群のみが提供されています。今のところは Scala meta に対応したマクロを提供するプラグインなどはなさそうです。(あったら教えて下さいな ^^)

Macro Paradise 3.0.0-M3

Macro Paradise 3.0.0-M3 では Scala meta で提案されている新しいスタイルのマクロを提供する…と思いきや、しません。Macro Paradise の機能は相変わらずマクロアノテーションのみです。では何が新しいかというと、まだ 1.0.0 には入っていない新しい Scala meta 形式でマクロアノテーションが作れる点です。

以下の例は、公式にある例で、 object に main メソッドを追加します。

import scala.meta._

class main extends scala.annotation.StaticAnnotation {
  inline def apply(defn: Any) = meta {
    val q"object $name { ..$stats }" = defn
    val main = q"""
      def main(args: Array[String]): Unit = { ..$stats }
    """
    q"object $name { $main }"
  }
}

以下に示す 2.1.0 風と比較すると幾つかの点が異なります。

class main extends scala.annotation.StaticAnnotation {
  def macroTransform(annottee: Any*): Any = macro Bundle.impl
}
import scala.reflect.macro.whitebox.Context
class Bundle(c: Context) {
  import c.universe._
  def impl(annottee: Tree*): Tree = { ... }
}

まず、マクロメソッドがインターフェースを担う macroTransform と変換メソッド (ここでは impl) に分かれていたのが apply 1つに集約されました。また、inlinemetaの2つのキーワードが導入されました。逆に言うと、この inline を使って定義された apply メソッドをもつ StaticAnnotation のサブクラスがプラグインにマクロアノテーションと認識されます。

仕組みを説明しましょう。構文解析フェーズで inline を検出すると以下のように機械的に書き換えます。

class main extends scala.annotation.StaticAnnotation {
  @scala.meta.internal.inline.inline def apply(defn: Any) = ???
}
object main$impl {
  def apply$impl(defn: metaTree) = {
    val q"object $name { ..$stats }" = defn
    val main = q"""
      def main(args: Array[String]): Unit = { ..$stats }
    """
    q"object $name { $main }"
  }
}

これによって、2.1.0 と同様の形式に直してしまうわけですね。

なお、metaは今のところは何の機能も果たしていませんが、将来的には必要な implicit を暗黙のうちに import させるブロックとして機能する予定のようです。ちなみに、厳密にはmetaはキーワードではなく、クロージャを取るメソッドという扱いのようですが、現行の Scala には暗黙の import をするブロックをサポートする機能が存在しないのでどうするつもりなのか興味があります。1

M1/M2/M3 の違い

M1 では M2 以降と違い以下のように object 内に定義していました。

import scala.meta._

object main {
  inline def apply()(defn: Any) = meta {
    val q"object $name { ..$stats }" = defn
    val main = q"""
      def main(args: Array[String]): Unit = { ..$stats }
    """
    q"object $name { $main }"
  }
}

これまでのマクロでは object か macro bundle という特別な形式の class にしか定義することができなかったためだと思われます。しかし、よく知っている人はわかるように object では @main は invalid なアノテーションで、相当不自然です。そのせいか、M2 からは普通のアノテーションに置き換えられました。

この副作用として、アノテーションの引数をマクロ内で直接取れるようになりました。引数というのは @compileTimeOnly("これはコンパイル時限定") のようなものの "これはコンパイル時限定" の部分のことです。2.1.0 では、c.prefix を呼ぶとアノテーション全体分の構文木が取れるので、それを自分でバラす必要がありました。割と面倒でした。

M2 では、今回紹介したようなマクロアノテーションに仕様変更が行われました。

M3 では、defvalのようにトップレベルでないものにもアノテーションが付けられるようになりました。実は M2 ではトップレベルしか対応していなかったのですね。はい。よく見るとissue#3にも書かれていました。実はこのバージョンアップは私の犯行です。前回の記事で新バージョンがあるという事実を見逃したリベンジをしようとしてできなかったので、ついカッとしてプルリクを投げてしまいました。反省はしています。

Scala meta ではまったところ

  • ParamTerm じゃないので Arg にならない
    • Defn.Defのパラメータを使ってメソッド適用の木を作るのがめんどい
  • Defn (メンバ?) と Decl (ローカル?) が違う
    • DefnBlock に入れられないので 2.1.0 の複数返すテクが使えない…
  • 依存ライブラリに指定されている 0.20.0 では ... 記法がバグってた
    • 1.0.0 では直っているらしいけどまだ確認できていない

  1. そのような機能を持つプロトタイプとしてInverse macro の副産物としてできたプラグインがあるので今度紹介記事を書きたいと思います。 

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
12