Relaxed extension methods (SIP 54) are not relaxed enough

Finally with the release of 3.4.0 I was able to test SIP 54 - Multi-Source Extension Overloads (aka relaxed extension methods) on my library (until very recently was blocked by regressions, so nightly version was not eligible).

It seems that SIP 54 was a good step forward, however not enough IMO.
Consider the following example (scastie link):

class Foo[T]
object Lib:
  extension (foo: Foo[Int])
    def bar: Unit = {}

import Lib.*
extension (foo: Foo[String])
 def bar: Unit = {}

val f = Foo[Int]()
f.bar //error

If a library defines an extension method on one type and I define a different extension method with the same name in my own code on a different type, the overloading mechanism chooses the method according to the classic scoping mechanism instead of choosing the best method for the extended type.

No error is generated and everything works as expected if we use implicit classes (scastie link):

class Foo[T]
object Lib:
  implicit class FooIntExt(foo: Foo[Int]):
    def bar: Unit = {}

import Lib.*
implicit class FooStringExt(foo: Foo[String]):
 def bar: Unit = {}

val f = Foo[Int]()
f.bar //works!

I think we’ve gotten used to implicit class semantics, and this should be preserved.

  • What happens if we convert the standard library to Scala 3 to use extension methods instead of implicit classes?
  • What happens if for the upcoming Named Tuples feature a library creates an extension method for its named tuples and its users have their own extension methods on different named tuples, but with the same name?

I think in both cases we get ambiguities or errors that can be a hassle to workaround. SIP 54 already special-cased extension methods for overloading. We need to take this even further and for extension methods apply priorities according to the extended type match, and only then apply the scoping priorities.

7 Likes

The same is true for this variant

class Foo
object Lib:
  implicit class FooIntExt(foo: Foo):
    def bar(a: Int): Unit = {}

import Lib.*
implicit class FooStringExt(foo: Foo):
 def bar(a: String): Unit = {}

val f = Foo()
f.bar(42) //works!

But apparently that was explicitly intended in the design of the SIP.