Search code examples
scala

How to add implicit arguments without breaking binary compatibility in Scala 2.13


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).


Solution

  • There are basically 2 problems outlined in this issue:

    1. How to solve the ambiguity error for external callers (which also relates to the desire to hide this legacy version from Scala API)
    2. How to solve the ambiguity error internally (how can the legacy version of 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).