Given a class with a function:
class Foo {
def func(a: A, b: B): R
}
How can I add an implicit argument to func
without breaking binary compatibility?
class Foo {
def func(a: A, b: B)(implicit c: C): R
}
With just the naive change, MiMa reports:
method func(mypackage.A, mypackage.B)myPackage.R in class mypackage.Foo does not have a correspondent in current version
Trying to add the original version leads to ambiguity errors:
class Foo {
def func(a: A, b: B): R = func(a, b)(defaultValueOfC)
def func(a: A, b: B)(implicit c: C): R
}
Any call site (including the legacy implementation of func
) gets hit with:
ambiguous reference to overloaded definition,
both method func in class Foo of type (a: mypackage.A, b: mypackage.B)(implicit c: mypackage.C): mypackage.R
and method func in class Foo of type (a: mypackage.A, b: mypackage.B): mypackage.R
match argument types (mypackage.A,mypackage.B)
It would also be nice to not have to keep the old definition as a public API (at least at the Scala source level).
There are basically 2 problems outlined in this issue:
func
call the new version with the implicit?)We can solve (1) by making the legacy implementation of func
package private. This abuses a quirk of how Scala maps to Java. Because Java does not have a notion of package private, package private methods are "public" in the Java byte code and thus can be used to polyfill old versions of methods for binary compatibility reasons.
class Foo {
private[mypackage] def func(a: A, b: B): R = func(a, b)(defaultValueOfC)
def func(a: A, b: B)(implicit c: C): R
}
We still run into problem 2, which we can solve by just creating a private implementation of func
with a different name that takes the implicit argument explicitly:
class Foo {
private[mypackage] def func(a: A, b: B): R = _func(a, b, defaultValueOfC)
def func(a: A, b: B)(implicit c: C): R = _func(a, b, c)
private def _func(a: A, b: B, c: C): R
}
One minor wart is that if you have any other callers of Foo.func
in package mypackage
, you will need to make _func
package private and call that instead.
In Scala 3, you can solve this a little bit more cleanly using @targetName
. @targetName
lets you tell the Scala compiler what Java name to lower something to, so you can just add the old version under a different Scala name but provide the old name as the @targetName
:
class Foo {
@scala.annotation.targetName("func")
private[mypackage] def legacyFunc(a: A, b: B): R = func(a, b)(new C)
def func(a: A, b: B)(implicit c: C): R
}
You then have the option of whether you want to use package private to hide legacyFunc
from the public Scala API while still having it show up in the Java byte code.
(Note: I have not tested this with MiMa in Scala 3).