「対消滅」って、何言ってるかよくわからないと思いますが、実際にある意味でその言葉がピッタリというか・・・。
以下のコードの実行結果は、Scala 2.13.16においてどうなるでしょうか?
package example case class A[B](value: Int) object A { implicit val int: A[Int] = new A[Int](2) } object C { implicit val int: A[Int] = new A[Int](3) } trait D { implicit val int: A[Int] = new A[Int](4) } object E extends D { import C._ def f = implicitly[A[Int]] } object Main { def main(args: Array[String]): Unit = { println(E.f) } }
答えは A(2)
です。コンパニオンにあるものが採用されます。
しかし、例えば以下のように変えてみましょう
-object E extends D { +object E { import C._
これだと A(3)
で、 import C._
したものが使われます。
では
object E extends D { - import C._ - def f = implicitly[A[Int]]
とするとどうなるでしょうか?これは A(4)
で、 extends D
したものが使われます。
つまり、本来は import C._
や extends D
でスコープに入れたもののほうが、ある意味でコンパニオンのものより強いはず?優先度が高いはず?なのに、衝突すると対消滅して、(本来、相対的に優先度が低いと思われる)コンパニオンのものが採用されてしまいます。
また、implicit valの名前が異なっているとどうなるのか?というと
object C { - implicit val int: A[Int] = new A[Int](3) + implicit val aaaaa: A[Int] = new A[Int](3) } trait D { - implicit val int: A[Int] = new A[Int](4) + implicit val bbbbb: A[Int] = new A[Int](4) }
これは、ある意味親切に(?)衝突してくれます
A.scala:20:21: ambiguous implicit values: [error] both value bbbbb in trait D of type example.A[Int] [error] and value aaaaa in object C of type example.A[Int] [error] match expected type example.A[Int] [error] def f = implicitly[A[Int]] [error] ^ [error] one error found [error] (Compile / compileIncremental) Compilation failed
これはだいぶ辛い厳しいわかりにく過ぎる挙動ですね・・・。ちなみに、Scala 3.7.1で最初のコードを実行すると A(4)
になって、コンパニオンのものが採用されるわけではなく、挙動が変わるようです。
Scala 3において、名前が衝突してると、compile errorになってくれる挙動は同じようですね。
その他、(無名の or 名前ありの)givenにしたらどうなるのか?compile option変えたら変わるのか?細かいscalaのversionによって挙動は違うのか?さらに別の優先度かもしれないものを追加したらどうなるか?
などなど、色々と気になることはありますが、そもそもこんな挙動の違いを覚えなくていいように、type classとしてのcoherenceを守って、1つの型に対して必ず1つの実態が定まる規約を絶対守るようにするのがオススメです。
仮に上級者だけこれを把握したところで、複数人でチームでやる時に、このような難し過ぎるルール全部を全員が覚えるのは厳し過ぎるし、覚えたところでlinterの補助がないとミスするだろうし、そもそもこういうパターンは、linter書くのも型を正確にみたりmacroの展開後のものを見る必要があってそんなに気軽に書けないので(書くならscalafixではなく、例えばwartremover)、そういう点でもだいぶ厳しいです。