このページでは、スコープ委譲について説明します。以前のページ、「ビルド定義」と「スコープ」を読んで理解していることを前提としています。
スコープの詳細をすべて説明したので、.valueルックアップを詳しく説明できます。これがこのページを初めて読む場合は、このセクションをスキップしても問題ありません。
これまでの学習内容をまとめると
Zeroがあります。ThisBuildがあります。TestはRuntimeを拡張し、RuntimeはCompile設定を拡張します。${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 / barがprojXで定義されていないにもかかわらず、sbtは依然としてTest / barを別のスコープ付きキーに解決することができ、その結果、fooは2として初期化されます。
sbtには、スコープ委譲と呼ばれる明確に定義されたフォールバック検索パスがあります。この機能により、より一般的なスコープで一度値を設定することで、複数のより具体的なスコープがその値を継承できます。
スコープ委譲のルールを以下に示します。
Zero(スコープのタスクなしバージョン)。Zero(スコープなしの設定軸と同じ)。ThisBuild、次にZero。このページの残りの部分で、各ルールを見ていきます。
言い換えれば、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の値は何ですか?
"foo-2.11.11"
"foo-2.12.18"
答えは"foo-2.11.11"です。.settings(...)内では、scalaVersionは自動的にprojA / Zero / Zeroにスコープされるため、packageBin / scalaVersionはprojA / Zero / packageBin / scalaVersionになります。その特定のスコープ付きキーは定義されていません。ルール2を使用することで、sbtはタスク軸をprojA / Zero / Zero(またはprojA / scalaVersion)としてZeroに置換します。そのスコープ付きキーは"2.11.11"として定義されています。
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になります。また、TestはRuntimeを拡張し、RuntimeはCompileを拡張することを思い出してください。
Test / barは定義されていませんが、ルール3により、sbtはprojX / Test / Zero、projX / Runtime / Zero、そしてprojX / Compile / Zeroでスコープされたbarを検索します。最後にCompile / barが見つかりました。
ThisBuild、次にZero。**演習B**:次のビルド定義が与えられた場合
ThisBuild / organization := "com.example"
lazy val projB = (project in file("b"))
.settings(
name := "abc-" + organization.value,
organization := "org.tempuri"
)
projB / nameの値は何ですか?
"abc-com.example"
"abc-org.tempuri"
答えは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の値は何ですか?
"foo-2.12.2"
"foo-2.11.11"
答えは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を実行した場合、何が表示されますか?
List()
List(-Ywarn-unused-import)
答えはList(-Ywarn-unused-import)です。ルール2はprojD / Compile / Zeroを見つけ、ルール3はprojD / Zero / consoleを見つけ、ルール4はThisBuild / Zero / Zeroを見つけます。ルール1は、サブプロジェクト軸にprojDがあり、設定軸がタスク軸よりも優先順位が高いため、projD / Compile / Zeroを選択します。
次に、Compile / scalacOptionsはscalacOptions.valueを参照するため、次にprojD / Zero / Zeroの委譲を見つける必要があります。ルール4はThisBuild / Zero / Zeroを見つけ、したがってList(-Ywarn-unused-import)に解決されます。
何が起こっているのかをすばやく確認したい場合があります。ここで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 / scalacOptionsをprojD / Compile / scalacOptionsによって提供されているかを示していることに注意してください。「委譲」の下には、優先順位順にリストされたすべての可能な委譲候補が記載されています!
projDスコープを持つすべてのスコープが最初にリストされ、次にThisBuild、Zeroが続きます。Compileスコープを持つスコープが最初にリストされ、次にZeroにフォールバックします。console /とタスクなしのものをリストします。スコープ委譲は、オブジェクト指向言語のクラス継承に似ているように見えますが、違いがあります。ScalaのようなOO言語では、トレイトShapeにdrawShapeという名前のメソッドがある場合、そのサブクラスは、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 は何を返しますか?
"2.12.2_0.1.0"
"2.11.11_0.1.0"
答えは 2.12.2_0.1.0 です。projE / version は ThisBuild / 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 は何を表示しますか?
"bippy-D4"
"bippy-D2-D4"
"bippy-D0-D3-D4"
答えは "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)
}
)