1. スコープ委譲(.valueルックアップ)

スコープ委譲(.valueルックアップ) 

このページでは、スコープ委譲について説明します。以前のページ、「ビルド定義」と「スコープ」を読んで理解していることを前提としています。

スコープの詳細をすべて説明したので、.valueルックアップを詳しく説明できます。これがこのページを初めて読む場合は、このセクションをスキップしても問題ありません。

これまでの学習内容をまとめると

  • スコープは、サブプロジェクト軸、設定軸、タスク軸の3つの軸のコンポーネントのタプルです。
  • いずれのスコープ軸に対しても、特別なスコープコンポーネントZeroがあります。
  • **サブプロジェクト軸**に対してのみ、特別なスコープコンポーネントThisBuildがあります。
  • TestRuntimeを拡張し、RuntimeCompile設定を拡張します。
  • build.sbtに配置されたキーは、デフォルトで${current subproject} / Zero / Zeroにスコープされます。
  • キーは/演算子を使用してスコープできます。

次に、次のビルド定義があるとします。

lazy val foo = settingKey[Int]("")
lazy val bar = settingKey[Int]("")

lazy val projX = (project in file("x"))
  .settings(
    foo := {
      (Test / bar).value + 1
    },
    Compile / bar := 1
  )

fooの設定ボディ内では、スコープ付きキーTest / barへの依存関係が宣言されています。しかし、Test / barprojXで定義されていないにもかかわらず、sbtは依然としてTest / barを別のスコープ付きキーに解決することができ、その結果、foo2として初期化されます。

sbtには、スコープ委譲と呼ばれる明確に定義されたフォールバック検索パスがあります。この機能により、より一般的なスコープで一度値を設定することで、複数のより具体的なスコープがその値を継承できます。

スコープ委譲ルール 

スコープ委譲のルールを以下に示します。

  • ルール1:スコープ軸には、サブプロジェクト軸、設定軸、タスク軸の順に優先順位があります。
  • ルール2:スコープが与えられた場合、委譲スコープは、次の順序でタスク軸を置換することで検索されます。指定されたタスクスコープ、次にZero(スコープのタスクなしバージョン)。
  • ルール3:スコープが与えられた場合、委譲スコープは、次の順序で設定軸を置換することで検索されます。指定された設定、その親、その親など、次にZero(スコープなしの設定軸と同じ)。
  • ルール4:スコープが与えられた場合、委譲スコープは、次の順序でサブプロジェクト軸を置換することで検索されます。指定されたサブプロジェクト、ThisBuild、次にZero
  • ルール5:委譲されたスコープ付きキーとその依存する設定/タスクは、元のコンテキストを引き継ぐことなく評価されます。

このページの残りの部分で、各ルールを見ていきます。

ルール1:スコープ軸の優先順位 

  • ルール1:スコープ軸には、サブプロジェクト軸、設定軸、タスク軸の順に優先順位があります。

言い換えれば、2つのスコープ候補が与えられた場合、サブプロジェクト軸でより具体的な値を持つ方が、設定またはタスクスコープに関係なく常に優先されます。同様に、サブプロジェクトが同じ場合、タスクスコープに関係なく、より具体的な設定値を持つ方が常に優先されます。「より具体的な」の定義については、さらにルールがあります。

ルール2:タスク軸の委譲 

  • ルール2:スコープが与えられた場合、委譲スコープは、次の順序でタスク軸を**置換**することで検索されます。指定されたタスクスコープ、次にZero(スコープのタスクなしバージョン)。

ここでは、任意の(xxx / yyy).valueが与えられた場合にsbtが委譲スコープを生成する方法に関する具体的なルールを示しています。覚えておいてください、私たちは任意の(xxx / yyy).valueが与えられた場合の検索パスを示そうとしています。

**演習A**:次のビルド定義が与えられた場合

lazy val projA = (project in file("a"))
  .settings(
    name := {
      "foo-" + (packageBin / scalaVersion).value
    },
    scalaVersion := "2.11.11"
  )

projA / nameの値は何ですか?

  1. "foo-2.11.11"
  2. "foo-2.12.18"
  3. その他?

答えは"foo-2.11.11"です。.settings(...)内では、scalaVersionは自動的にprojA / Zero / Zeroにスコープされるため、packageBin / scalaVersionprojA / Zero / packageBin / scalaVersionになります。その特定のスコープ付きキーは定義されていません。ルール2を使用することで、sbtはタスク軸をprojA / Zero / Zero(またはprojA / scalaVersion)としてZeroに置換します。そのスコープ付きキーは"2.11.11"として定義されています。

ルール3:設定軸の検索パス 

  • ルール3:スコープが与えられた場合、委譲スコープは、次の順序で設定軸を置換することで検索されます。指定された設定、その親、その親など、次にZero(スコープなしの設定軸と同じ)。

その例は、前に見たprojXです。

lazy val foo = settingKey[Int]("")
lazy val bar = settingKey[Int]("")

lazy val projX = (project in file("x"))
  .settings(
    foo := {
      (Test / bar).value + 1
    },
    Compile / bar := 1
  )

完全なスコープをもう一度記述すると、projX / Test / Zeroになります。また、TestRuntimeを拡張し、RuntimeCompileを拡張することを思い出してください。

Test / barは定義されていませんが、ルール3により、sbtはprojX / Test / ZeroprojX / Runtime / Zero、そしてprojX / Compile / Zeroでスコープされたbarを検索します。最後にCompile / barが見つかりました。

ルール4:サブプロジェクト軸の検索パス 

  • ルール4:スコープが与えられた場合、委譲スコープは、次の順序でサブプロジェクト軸を置換することで検索されます。指定されたサブプロジェクト、ThisBuild、次にZero

**演習B**:次のビルド定義が与えられた場合

ThisBuild / organization := "com.example"

lazy val projB = (project in file("b"))
  .settings(
    name := "abc-" + organization.value,
    organization := "org.tempuri"
  )

projB / nameの値は何ですか?

  1. "abc-com.example"
  2. "abc-org.tempuri"
  3. その他?

答えはabc-org.tempuriです。したがって、ルール4に基づいて、最初の検索パスはprojB / Zero / Zeroにスコープされたorganizationであり、これはprojB"org.tempuri"として定義されています。これは、ビルドレベルの設定ThisBuild / organizationよりも優先順位が高くなります。

スコープ軸の優先順位、再び 

**演習C**:次のビルド定義が与えられた場合

ThisBuild / packageBin / scalaVersion := "2.12.2"

lazy val projC = (project in file("c"))
  .settings(
    name := {
      "foo-" + (packageBin / scalaVersion).value
    },
    scalaVersion := "2.11.11"
  )

projC / nameの値は何ですか?

  1. "foo-2.12.2"
  2. "foo-2.11.11"
  3. その他?

答えはfoo-2.11.11です。projC / Zero / packageBinにスコープされたscalaVersionは定義されていません。ルール2はprojC / Zero / Zeroを見つけます。ルール4はThisBuild / Zero / packageBinを見つけます。この場合、ルール1は、サブプロジェクト軸に定義されたより具体的な値であるprojC / Zero / Zero"2.11.11"として定義されている)が優先されることを示しています。

**演習D**:次のビルド定義が与えられた場合

ThisBuild / scalacOptions += "-Ywarn-unused-import"

lazy val projD = (project in file("d"))
  .settings(
    test := {
      println((Compile / console / scalacOptions).value)
    },
    console / scalacOptions -= "-Ywarn-unused-import",
    Compile / scalacOptions := scalacOptions.value // added by sbt
  )

projD/testを実行した場合、何が表示されますか?

  1. List()
  2. List(-Ywarn-unused-import)
  3. その他?

答えはList(-Ywarn-unused-import)です。ルール2はprojD / Compile / Zeroを見つけ、ルール3はprojD / Zero / consoleを見つけ、ルール4はThisBuild / Zero / Zeroを見つけます。ルール1は、サブプロジェクト軸にprojDがあり、設定軸がタスク軸よりも優先順位が高いため、projD / Compile / Zeroを選択します。

次に、Compile / scalacOptionsscalacOptions.valueを参照するため、次にprojD / Zero / Zeroの委譲を見つける必要があります。ルール4はThisBuild / Zero / Zeroを見つけ、したがってList(-Ywarn-unused-import)に解決されます。

inspectコマンドは委譲を表示します 

何が起こっているのかをすばやく確認したい場合があります。ここでinspectを使用できます。

sbt:projd> inspect projD / Compile / console / scalacOptions
[info] Task: scala.collection.Seq[java.lang.String]
[info] Description:
[info]  Options for the Scala compiler.
[info] Provided by:
[info]  ProjectRef(uri("file:/tmp/projd/"), "projD") / Compile / scalacOptions
[info] Defined at:
[info]  /tmp/projd/build.sbt:9
[info] Reverse dependencies:
[info]  projD / test
[info]  projD / Compile / console
[info] Delegates:
[info]  projD / Compile / console / scalacOptions
[info]  projD / Compile / scalacOptions
[info]  projD / console / scalacOptions
[info]  projD / scalacOptions
[info]  ThisBuild / Compile / console / scalacOptions
[info]  ThisBuild / Compile / scalacOptions
[info]  ThisBuild / console / scalacOptions
[info]  ThisBuild / scalacOptions
[info]  Zero / Compile / console / scalacOptions
[info]  Zero / Compile / scalacOptions
[info]  Zero / console / scalacOptions
[info]  Global / scalacOptions

「提供元」がどのようにprojD / Compile / console / scalacOptionsprojD / Compile / scalacOptionsによって提供されているかを示していることに注意してください。「委譲」の下には、優先順位順にリストされたすべての可能な委譲候補が記載されています!

  • サブプロジェクト軸にprojDスコープを持つすべてのスコープが最初にリストされ、次にThisBuildZeroが続きます。
  • サブプロジェクト内では、設定軸にCompileスコープを持つスコープが最初にリストされ、次にZeroにフォールバックします。
  • 最後に、タスク軸スコープは、指定されたタスクスコープconsole /とタスクなしのものをリストします。

.valueルックアップと動的ディスパッチ 

  • ルール5:委譲されたスコープ付きキーとその依存する設定/タスクは、元のコンテキストを引き継ぐことなく評価されます。

スコープ委譲は、オブジェクト指向言語のクラス継承に似ているように見えますが、違いがあります。ScalaのようなOO言語では、トレイトShapedrawShapeという名前のメソッドがある場合、そのサブクラスは、Shapeトレイトの他のメソッドによってdrawShapeが使用されている場合でも、その動作をオーバーライドできます。これは動的ディスパッチと呼ばれます。

しかし、sbtでは、スコープ委譲はスコープをより一般的なスコープ(プロジェクトレベルの設定をビルドレベルの設定など)に委譲できますが、そのビルドレベルの設定はプロジェクトレベルの設定を参照できません。

**演習E**:次のビルド定義が与えられた場合

lazy val root = (project in file("."))
  .settings(
    inThisBuild(List(
      organization := "com.example",
      scalaVersion := "2.12.2",
      version      := scalaVersion.value + "_0.1.0"
    )),
    name := "Hello"
  )

lazy val projE = (project in file("e"))
  .settings(
    scalaVersion := "2.11.11"
  )

projE / version は何を返しますか?

  1. "2.12.2_0.1.0"
  2. "2.11.11_0.1.0"
  3. その他?

答えは 2.12.2_0.1.0 です。projE / versionThisBuild / version に委譲し、これは ThisBuild / scalaVersion に依存しています。このため、ビルドレベルの設定は、主に単純な値の代入に制限する必要があります。

練習問題 F: 次のビルド定義を想定します

ThisBuild / scalacOptions += "-D0"
scalacOptions += "-D1"

lazy val projF = (project in file("f"))
  .settings(
    compile / scalacOptions += "-D2",
    Compile / scalacOptions += "-D3",
    Compile / compile / scalacOptions += "-D4",
    test := {
      println("bippy" + (Compile / compile / scalacOptions).value.mkString)
    }
  )

projF / test は何を表示しますか?

  1. "bippy-D4"
  2. "bippy-D2-D4"
  3. "bippy-D0-D3-D4"
  4. その他?

答えは "bippy-D0-D3-D4" です。これは、Paul Phillipsによって作成された練習問題のバリエーションです。

これは、すべてのルールをうまく示しています。なぜなら、someKey += "x" は以下のように展開されるからです。

someKey := {
  val old = someKey.value
  old :+ "x"
}

古い値を取得しようとすると委譲が発生し、ルール5により、別のスコープのキーに移動します。まず += を削除し、古い値の委譲に注釈を付けます。

ThisBuild / scalacOptions := {
  // Global / scalacOptions <- Rule 4
  val old = (ThisBuild / scalacOptions).value
  old :+ "-D0"
}

scalacOptions := {
  // ThisBuild / scalacOptions <- Rule 4
  val old = scalacOptions.value
  old :+ "-D1"
}

lazy val projF = (project in file("f"))
  .settings(
    compile / scalacOptions := {
      // ThisBuild / scalacOptions <- Rules 2 and 4
      val old = (compile / scalacOptions).value
      old :+ "-D2"
    },
    Compile / scalacOptions := {
      // ThisBuild / scalacOptions <- Rules 3 and 4
      val old = (Compile / scalacOptions).value
      old :+ "-D3"
    },
    Compile / compile / scalacOptions := {
      // projF / Compile / scalacOptions <- Rules 1 and 2
      val old = (Compile / compile / scalacOptions).value
      old :+ "-D4"
    },
    test := {
      println("bippy" + (Compile / compile / scalacOptions).value.mkString)
    }
  )

これは以下になります。

ThisBuild / scalacOptions := {
  Nil :+ "-D0"
}

scalacOptions := {
  List("-D0") :+ "-D1"
}

lazy val projF = (project in file("f"))
  .settings(
    compile / scalacOptions := List("-D0") :+ "-D2",
    Compile / scalacOptions := List("-D0") :+ "-D3",
    Compile / compile / scalacOptions := List("-D0", "-D3") :+ "-D4",
    test := {
      println("bippy" + (Compile / compile / scalacOptions).value.mkString)
    }
  )