Search code examples
scalacompiler-errorssbtscala-breeze

Workaround for a scala.language.dynamics bug in scala 2.10 compiler


I've been trying to generate an SBT build for an open source project that I'd like to reference from within my project, and I ran into what seems to be a compiler bug.

The following code compiles and runs as expected in eclipse/scala-ide, but the scala 2.10.6 compiler is unable to digest it:

package foo

import scala.language.dynamics

object Caller extends App {
  val client = new Client() // initialise an R interpreter
  client.x = 1.0
}
class Client extends Dynamic {
  var map = Map.empty[String, Any]
  def selectDynamic(name: String) = map get name getOrElse sys.error("field not found")
  def updateDynamic(name: String)(value: Any) { map += name -> value }
}

Here's my build.sbt:

scalaVersion := "2.10.6"

libraryDependencies++= Seq(
  "org.scalanlp" %% "breeze" % "0.12"
)

When I specify scalaVersion := 2.10.6, I get the following compile error:

[error] /home/philwalk/dynsbt/src/main/scala/foo/Caller.scala:8: type mismatch;
[error]  found   : foo.Caller.client.type (with underlying type foo.Client)
[error]  required: ?{def x: ?}
[error] Note that implicit conversions are not applicable because they are ambiguous:
[error]  both method any2Ensuring in object Predef of type [A](x: A)Ensuring[A]
[error]  and method any2ArrowAssoc in object Predef of type [A](x: A)ArrowAssoc[A]
[error]  are possible conversion functions from foo.Caller.client.type to ?{def x: ?}
[error]   client.x = Seq("a","b","c")
[error]   ^
[error] one error found
[error] (compile:compileIncremental) Compilation failed
[error] Total time: 3 s, completed May 3, 2016 11:03:08 AM

With scalaVersion := 2.11.8, no problems, although I need to cross-compile, so that's not a work-around.

Another clue is that I can hide the problem by changing this line of code:

client.x = 1.0

to this:

client.xx = 1.0

I also see the problem when I compile directly with scalac 2.10.6.

As a workaround, I could refactor the project to use field names longer than a single character, although because it's not my project, I'm somewhat constrained on what I can accept as a workaround. Also, it's a breeze.linalg project and it would be a serious limitation to disallow single character matrix and vector names.

It took a few hours to boil the problem down to this code fragment from a larger project, and I would prefer not to impose limitations on the scala 2.10 version of this open source library. Since this bug seems to have been fixed in scala 2.11, I'm assuming it was decided not to backport the fix to 2.10.

I changed the title to reflect the existence of a workaround (longer field name).


Solution

  • This is Scala 2.10's fault, not sbt's.

    The problem is that, in Predef, whose content is imported in every single Scala file, there are two problematic classes: Ensuring and ArrowAssoc. The members of these two classes are available through implicit conversions on any type of value. ArrowAssoc, for example, is the reason you can do 1 -> 2 to construct a tuple (1, 2).

    Now, these classes had, in 2.10, the very unfortunate property of declaring a member named x! Even though deprecated in 2.10, there existence is a serious problem for usages of scala.Dynamic.

    In your code, client.x = 1.0 first tests whether client.x exists as a val/getter on client's type. It doesn't, really, but it would be available if we used the implicit conversions in Predef to convert it to Ensuring or ArrowAssoc. Since implicit conversions have a higher priority than the selectDynamic treatment, the Scala compiler tries to use them. But since there are two equally valid conversions, they are ambiguous, and you get a compile error.

    In summary, this is an unfortunate consequence of 2 facts:

    • There are implicit conversions providing a member x on everything
    • That conversion has priority over the Dynamic treatment.

    The way to solve this, in your instance, is to declare explicitly x in Client, as a forwarder to selectDynamic and updateDynamic:

    class Client extends Dynamic {
      var map = Map.empty[String, Any]
      def selectDynamic(name: String) = map get name getOrElse sys.error("field not found")
      def updateDynamic(name: String)(value: Any) { map += name -> value }
    
      // Work around the annoying implicits in Predef in Scala 2.10.
      def x: Any = selectDynamic("x")
      def x_=(value: Any): Unit = updateDynamic("x")(value)
    }
    

    Now, client.x will of course use the explicitly declared x and its setter x_= in Client, which will delegate to selectDynamic/updateDynamic.