Apologies if this is something that’s already been brought up, but I’ve not been able to find mention of this interesting issue elsewhere. Basically, what I’ve found is that overloaded extension methods sometimes work, and sometimes not.
Overloaded extension methods seem to work beautifully when either:
- There is no inheritance going on
- All overloads are declared on the parent and the child doesn’t touch that overload family
- The child overrides every overload from the parent
Otherwise, compilation fails when trying to use overloaded extension methods. The main failure mode seems to be that a child class modifying the family of overloads in any way causes all the overloads from the parent class to be invisible to the compiler.
Here’s some minimal code that reproduces the issue in Scala 3.4.2:
trait Foo:
extension (a: Int)
def foo(other: Int): Int = a + other
def foo(other: String): (Int, String) = (a, other)
object Bar extends Foo: // Having the TWO override lines immediately below both commented or both uncommented results in expected behaviour
// extension (a: Int) override def foo(other: Int): Int = a + other // Uncommenting just this line results in a failure to compile on the second print statement
// extension (a: Int) override def foo(other: String): (Int, String) = (a, other) // Uncommenting just this line results in a failure to compile on the first print statement
// extension (a: Int) def foo(other: Double): Double = other // Uncommenting just this unused additional overload causes a failure to compile on both of the first two print statements, but works fine if the other two overloads are also uncommented
extension (a: Int) def bar(other: Int): Int = other // Adding or not adding this non-overload extension has no effect on the result
end Bar
import Bar.foo
import Bar.bar
println(1.foo(1)) // Expected: 2
println(1.foo("1")) // Expected: (1, 1)
println(1.bar(1)) // Expected: 1; always works
Interestingly, it seems like part of an overload family can still work after modification in a child class, but only if that part has an incompatible extension target.
For instance, consider
println("a".foo("b"))
If we define the following extension on Foo
:
extension(a: String) def foo(other: String): (String, String) = (a, other)
then our println
continues to work regardless of whether any of the lines in Bar
are uncommented because (I think) a String
is never an Int
.
On the other hand, the very generic
extension[L](a: L) def foo[R](other: R): (L, R) = (a, other)
which should also handle our println
case gets messed up by any of the extension(a: Int)
overloads, because (I think) the compiler prefers Int
to generic L
. If none of those overloads are performed, this extension works though.