search term:

ifdef 0.3.0: Scala における条件付きコンパイル

@ifdef は Scala コンパイラ・プラグインで、Scala 言語における条件付きコンパイルを実装する。ifdef 0.3.0 では Scala.JS と Scala Native のサポートを追加した。

Scala Version JVM JS (1.x) Native (0.5.x)
3.x
2.13.x
2.12.x

セットアップ

project/plugins.sbt に以下を追加する:

addSbtPlugin("com.eed3si9n.ifdef" % "sbt-ifdef" % "0.3.0")

ソースは https://github.com/eed3si9n/ifdef 参照。

条件付きコンパイル

Rust 言語は条件付きソースコードを以下のように定義する:

特定の条件によりソースコード全体の一部に含まれたり含まれなかったりするコード

ifDefDeclaration は、名前もしくはキーと値のペアで、宣言されているかされていないかで条件を制御する。sbt-ifdef 0.3.0 は 2つの宣言を組み込みで提供する:

  1. ビルドのコンフィギュレーション名: compiletest
  2. scalaBinaryVersion: scalaBinaryVersion:3

このあたりは想像をはたらかせて、旧来はクロスビルドが必要だったほかの軸などにも試してみてほしい。

ソース内単体テスト

Rust 言語での条件付きコンパイルの使われ方の 1つとして、ライブラリコード内に単体テストを埋め込むというものがある:

package example

import com.eed3si9n.ifdef.ifdef

class A:
  def foo: Int = 42
end A

@ifdef("test")
class ATest extends munit.FunSuite:
  test("foo"):
    val actual = new A().foo
    val expected = 42
    assertEquals(actual, expected)
end ATest

単体テストの多くは、別のソースよりもこのように埋め込んだ方が見通しが良くなると思う。

Scala クロス・ビルド

@ifdef@ifndef を使った scalaBinaryVersion の例を見てみる:

package example

import com.eed3si9n.ifdef.{ ifdef, ifndef }
import java.util.{ Map => JMap }

class A {
  @ifdef("scalaBinaryVersion:2.12")
  def convertMapToScala[K, V](jmap: JMap[K, V]): Map[K, V] = {
    // -Xsource:3 を使うことで Scala 2.12 からも * を使える。ナイス
    import scala.collection.JavaConverters.*
    Map.empty ++ jmap.asScala
  }

  @ifndef("scalaBinaryVersion:2.12")
  def convertMapToScala[K, V](jmap: JMap[K, V]): Map[K, V] = {
    import scala.jdk.CollectionConverters.*
    Map.empty.concat(jmap.asScala)
  }
}

上の例は実は Stefan Zeiger さんが、SIP-NN Language support for conditional compilation (現在は閉じてる) で条件付きコンパイルの提案を行ったときに引用したものと同じ例だ。この例は @ifdef@ifndef が、タイパー (typer) フェーズ以前に処理していることをデモしている。なぜなら、Scala 2.12 は scala.jdk.CollectionConverters.* のことを知らないので、普通のコンパイルだとここで失敗するからだ。

0.3.0 から入ったもの

参照