倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

sbt でも Scala 2.11 でマクロしたい!

sbt プロジェクトで Scala 2.11 のマクロを試そうと思って設定してたんだけど、マクロの仕様上、単純な sbt プロジェクトではうまくいかなかったので必要な設定をメモ。

sbt -0.13.0 までのドキュメントには「Macro Projects」という項目があったのですが

0.13.1 からは目次からなくなったので(記事自体はある模様)、マクロ用の設定がいらなくなったのかとちょっと喜んだんだけど、全くそんなことはなかったようで。 どこかの項目に吸収されたのかな?

この記事では使用した sbt のバージョンは 0.13.6 です。

修正

マクロを定義するサブプロジェクトの build.sbt 内での名前を「macroSub」に変更しました。 マクロコードを配置するディレクトリは変わらず「macro」です。

前のままでも構わないのですが、IntelliJ IDEA (要プラグイン)で build.sbt を開くと「macro」を予約後として誤認識してしまうためです。 その他の IDE の対応は知りませんが。

参考

設定概要

Scala ではマクロの仕様上、定義と使用を別のモジュールにしないといけないようで
sbt では別プロジェクトにしてやる必要があります。 まぁ、基本的には普通のマルチプロジェクトにしてあげればいいだけですが、sbt のマルチプロジェクト自体にあんまり慣れてないのでそのあたりの設定もしていきます。

sbt 0.13.0 までのドキュメントに載っていた「Macro Projects」では、ルートプロジェクトが通常の(src ディレクトリなどを持つ) sbt プロジェクトで、そのサブプロジェクトとしてマクロコードを書くプロジェクトを定義しているので、それに従って設定します。 build.sbt で設定するのは概ね以下の項目:

  • マクロを定義するサブプロジェクト macroSub & ルートプロジェクトのプロジェクト依存性の設定
  • Scala のバージョンを 2.11.4 に設定
  • マクロを使用するために scalac のオプションに "-language:experimental.macros" を追加
  • パッケージングに macroSub プロジェクトのソースコードバイトコードを含める

build.sbt の設定

では、上記の設定項目を順に見ていきましょう。 プロジェクト名は mymacro としておきましょう。 ちなみにプロジェクトのディレクトリ構成は以下のようになってます:

mymacro/ ・・・ プロジェクトのルート・ディレクトリ

  • build.sbt ・・・ これを設定
  • src/
  • macro/ ・・・ マクロのためのサブプロジェクト(macroSub プロジェクト)

以下で build.sbt の設定を見ていきます。

サブプロジェクト macroSub
まずはマクロを配置するサブプロジェクト macroSub の設定しましょう。 サブプロジェクト macroSub の作成は「lazy val macroSub = project in file("macro")」としますが*1、マクロを使うためにはさらに scala-reflect をライブラリとして依存性に加える必要があります:

lazy val macroSub = (project in file("macro")).
  settings(
    libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
  )

「scalaVersion.value」はプロジェクトの Scala バージョンを参照しています。

次はルート・プロジェクトの設定。 ルート・プロジェクトは macroSub プロジェクトに依存するように設定します:

lazy val root = (project in file(".")).
  aggregate(macroSub).  // 別に必要なし
  dependsOn(macroSub)

依存性は「.dependsOn(macroSub)」によって設定します。 「.aggregate(macroSub)」は、まぁ別にいりません。 設定項目の確認とかにちょっと便利なので設定してます。

Scala のバージョン
Scala のバージョンは scalaVersion によって定義できますが、単に

scalaVersion := "2.11.4"

とすると、ルート・プロジェクトの scalaVersion しか設定されません。 sbt コンソールで「show scalaVersion」(もしくは単に「scalaVersion」)を実行すると*2

> show scalaVersion
[info] macroSub/*:scalaVersion
[info] 2.10.4
[info] root/*:scalaVersion
[info] 2.11.4

となり、macroSub プロジェクトの scalaVersion は 2.10.4 であることがわかります。 これは macroSub プロジェクトでは scalaVersion が設定されていないとみなされ、sbt-0.13 のデフォルト Scala バージョンである 2.10.4 が使われているのが理由です。 これを解決するためには macroSub プロジェクトで別途 scalaVersion を設定してもいいんですが、大抵は全プロジェクトで同一の Scala バージョンを使うと思うので、以下のように Global スコープでまとめて定義しておいた方が無難:

scalaVersion in Global := "2.11.4"

リロードして scalaVersion を再度実行すると

> reload
> show scalaVersion
[info] macroSub/*:scalaVersion
[info] 2.11.4
[info] root/*:scalaVersion
[info] 2.11.4

となり、macroSub プロジェクトでも scalaVersion がきちんと設定されていることが分かります。

ちなみに、同様のことが scalaVersion 以外の設定項目でも当てはまります。 例えば、マルチプロジェクト全体で同一の version を設定したい場合は

version in Global := "1.0"

とします。

Scalac オプション
sbt 関係なく、マクロを使うためには scala-reflect を依存性に含める以外に以下のいずれかの設定が必要です:

  • マクロを定義・実装しているコードに明示的に「import language.experimental.macros」を追加する
  • scalac のコンパイルオプションに「-language:experimental.macros」を追加する

各マクロコードに import 文を書くのは面倒なので(いずれ実験コードでなくなったときにいらなくなるかもしれないし)、ここでは後者の設定をします:

scalacOptions in Global += "-language:experimental.macros"

他のコンパイル・オプションがある場合は以下のようにします:

scalacOptions in Global ++= Seq(
  "-deprecation",
  "-encoding", "UTF-8",
  "-language:experimental.macros"
)

もしくは、このコンパイル・オプションが必要なのは macroSub プロジェクトだけなので、macroSub プロジェクトの設定を

lazy val macroSub = project.
  settings(
    scalacOptions += "-language:experimental.macros",
    libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
  )

のようにしても構いません。 プロジェクトに複数の設定 (settings) を行いたい場合は、単にコンマ (,) で区切って書くだけで OK。

パッケージング
パッケージングの際に macroSub プロジェクトのバイトコードソースコードをルートプロジェクトのものとまとめてパッケージングしたい場合には、ルートプロジェクトの設定 (settings) に以下のようなコードを追加しておきます:

lazy val root = (project in file(".")).
  aggregate(macroSub).
  dependsOn(macroSub).
  // 以下を追加
  settings(
    mappings in (Compile, packageBin) ++= mappings.in(macro, Compile, packageBin).value,
    mappings in (Compile, packageSrc) ++= mappings.in(macro, Compile, packageSrc).value
  )

これで設定完了。

サンプル build.sbt

以上の設定をまとめると build.sbt のテンプレートは以下のような感じ。 個人的に常に設定している項目 (javaVersion, encoding) をちょっと入れたのでごちゃっとしちゃってるかな?:

name := "mymacro"

version in Global := "1.0"

scalaVersion in Global := "2.11.4"

//***** Custom settings *****
val javaVersion = settingKey[String]("javac source/target version")

val encoding = settingKey[String]("source encoding")

javaVersion in Global := "1.8"

encoding in Global := "UTF-8"

//***** Projects *****
lazy val root = (project in file(".")).
  aggregate(macroSub).
  dependsOn(macroSub).
  settings(
    mappings in (Compile, packageBin) ++= mappings.in(macroSub, Compile, packageBin).value,
    mappings in (Compile, packageSrc) ++= mappings.in(macroSub, Compile, packageSrc).value
  )

lazy val macroSub = (project in file("macro")).
  settings(
    scalacOptions += "-language:experimental.macros",
    libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
  )

//***** Options & Dependencies *****
javacOptions in Global ++= Seq(
  "-source", javaVersion.value,
  "-target", javaVersion.value,
   "-encoding", encoding.value
)

scalacOptions in Global ++= Seq(
  "-deprecation",
  "-encoding", encoding.value
)

libraryDependencies in Global ++= Seq(
  "org.scala-lang" % "scala-library" % scalaVersion.value,
  "org.scalatest" % "scalatest_2.11" % "2.2.2" % Test
)

//***** Running *****
fork in Global := true

//***** Packaging *****
//mainClass in Compile := Some("org.sample.Main")
//mainClass in (Compile, packageBin) := Some("org.sample.Main")
//mainClass in (Compile, run) := Some("org.sample.Main")

crossPaths in Global := false
  • javaVersion と encoding はそれぞれ java ソースのバージョンとエンコーディングです。 sbt って独自キーの定義と値の設定が1行で書けないもんなのかな?
  • fork, mainClass, crossPath は実行やパッケージングに使う設定。 スコープとかこれでいいのかあんまり考えてません。

マクロの簡単なサンプルは「参考」に示したリンク先にいくつかあるので試して見て下さい。

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築

Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築

*1:もしサブディレクトリ名と変数名が同じ、つまり変数名を「macroSub」ではなく「macro」にするなら、「lazy val macro = project」だけで OK です。 ここでは macro が予約後っぽいので(実際には違いますが、IDE が誤認したりする)、変数名を変えました。

*2:ルートプロジェクトの設定で aggregate を指定していないと、ルートプロジェクトの scalaVersion しか表示されません。 この場合、「show macro/*:scalaVersion」とすれば macroSub プロジェクトの scalaVersion が表示されます。