このページでは、スコープ委譲について説明します。以前のページ、「ビルド定義」と「スコープ」を読んで理解していることを前提としています。
スコープの詳細をすべて説明したので、.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)
}
)