implicitの名前が衝突しているとScala 2.13では対消滅して優先度低いものが採用されることがある

対消滅」って、何言ってるかよくわからないと思いますが、実際にある意味でその言葉がピッタリというか・・・。

以下のコードの実行結果は、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)、そういう点でもだいぶ厳しいです。