1. マクロプロジェクト

マクロプロジェクト 

はじめに 

マクロを使用する際にいくつかの一般的な問題が発生します。

  1. コンパイラの現在のマクロ実装では、マクロの実装が使用される前にコンパイルされる必要があります。解決策は通常、マクロをサブプロジェクトまたは独自の設定に配置することです。
  2. マクロ実装をそれを使用するメインコードと一緒に配布する必要がある場合もあれば、実装をまったく配布しない方が良い場合もあります。

このページの残りの部分では、これらの問題に対する解決策の例を示します。

プロジェクトの関係の定義 

マクロの実装は、macro/ ディレクトリのサブプロジェクトに入ります。core/ ディレクトリのコアプロジェクトは、このサブプロジェクトに依存し、マクロを使用します。この構成は、次のビルド定義に示されています。build.sbt

lazy val commonSettings = Seq(
  scalaVersion := "2.12.18",
  organization := "com.example"
)
lazy val scalaReflect = Def.setting { "org.scala-lang" % "scala-reflect" % scalaVersion.value }

lazy val core = (project in file("core"))
  .dependsOn(macroSub)
  .settings(
    commonSettings,
    // other settings here
  )

lazy val macroSub = (project in file("macro"))
  .settings(
    commonSettings,
    libraryDependencies += scalaReflect.value
    // other settings here
  )

これにより、マクロの実装が macro/src/main/scala/ に、テストが macro/src/test/scala/ に配置されることが指定されます。また、マクロの実装にはコンパイラへの依存関係が必要であることも示しています。マクロの例として、macrocosmdesugar を使用します。macro/src/main/scala/demo/Demo.scala

package demo

import language.experimental.macros
import scala.reflect.macros.blackbox.Context

object Demo {

  // Returns the tree of `a` after the typer, printed as source code.
  def desugar(a: Any): String = macro desugarImpl

  def desugarImpl(c: Context)(a: c.Expr[Any]) = {
    import c.universe._

    val s = show(a.tree)
    c.Expr(
      Literal(Constant(s))
    )
  }
}

macro/src/test/scala/demo/Usage.scala:

package demo

object Usage {
   def main(args: Array[String]): Unit = {
      val s = Demo.desugar(List(1, 2, 3).reverse)
      println(s)
   }
}

これはコンソールで実行できます

$ sbt
> macroSub/Test/run
scala.collection.immutable.List.apply[Int](1, 2, 3).reverse

実際のテストは、通常どおり macro/test で定義および実行できます。

メインプロジェクトは、テストと同じ方法でマクロを使用できます。たとえば、

core/src/main/scala/MainUsage.scala:

package demo

object Usage {
   def main(args: Array[String]): Unit = {
      val s = Demo.desugar(List(6, 4, 5).sorted)
      println(s)
   }
}
$ sbt
> core/run
scala.collection.immutable.List.apply[Int](6, 4, 5).sorted[Int](math.this.Ordering.Int)

共通インターフェース 

場合によっては、マクロ実装とマクロの使用が共通のコードを共有する必要があります。この場合、共通コード用の別のサブプロジェクトを宣言し、メインプロジェクトとマクロサブプロジェクトに新しいサブプロジェクトに依存させます。たとえば、上記のプロジェクト定義は次のようになります。

lazy val commonSettings = Seq(
  scalaVersion := "2.12.18",
  organization := "com.example"
)
lazy val scalaReflect = Def.setting { "org.scala-lang" % "scala-reflect" % scalaVersion.value }

lazy val core = (project in file("core"))
  .dependsOn(macroSub, util)
  .settings(
    commonSettings,
    // other settings here
  )

lazy val macroSub = (project in file("macro"))
  .dependsOn(util)
  .settings(
    commonSettings,
    libraryDependencies += scalaReflect.value
    // other settings here
  )

lazy util = (project in file("util"))
  .settings(
    commonSettings,
    // other setting here
  )

util/src/main/scala/ のコードは、macroSub プロジェクトと main プロジェクトの両方で使用できます。

配布 

マクロコードをコアコードに含めるには、マクロサブプロジェクトからコアプロジェクトへのバイナリおよびソースマッピングを追加します。また、マクロサブプロジェクトは公開時にコアプロジェクトの依存関係から削除する必要があります。たとえば、上記の core プロジェクト定義は次のようになります。

lazy val core = (project in file("core"))
  .dependsOn(macroSub % "compile-internal, test-internal")
  .settings(
    commonSettings,
    // include the macro classes and resources in the main jar
    Compile / packageBin / mappings ++= (macroSub / Compile / packageBin / mappings).value,
    // include the macro sources in the main source jar
    Compile / packageSrc / mappings ++= (macroSub / Compile / packageSrc / mappings).value
  )

マクロ実装の公開を無効にしたい場合があります。これは、publish および publishLocal をオーバーライドして何も行わないようにすることで行われます。

lazy val macroSub = (project in file("macro"))
  .settings(
    commonSettings,
    libraryDependencies += scalaReflect.value,
    publish := {},
    publishLocal := {}
  )

ここで説明する手法は、前のセクションで説明した共通インターフェースにも使用できます。