Search code examples
scalaimplicitscala-compiler

Scala Implicit Method Compilation when Method Tries to Access Non-Existing Case Class Members


I did run into an scala compiler issue with implicit methods. The scenario is quite easy. The task of the implicit method is to turn an object of the case class A into an object of the case class B. The implicit method implementation accesses a case class member of A which does not exist. If the case class member does not exist at all in case class A or B (e.g. foobar), the compiler throws an error. If the case class member does exist in case class B, the compiler does not throw an error, even if I access case class A with this name (i.e. member x).

I am using Scala in version 2.13.1. Currently, 2.13.2 is the most current version.

The following code shows the scenario in more detail. The following code will lead to a compiler error.

package Hokuspokus

object ImplicitMagic extends App {

  case class A(a: String, b: String, c: String)
  case class B(d: String, e: String, f: String, x: String)

  implicit def AtoB: A => B = a => B(a.a, a.b, a.c, a.foobar)

  def print(b: B): Unit = {
    System.out.println("Print" + b.d)
  }

  val a = A("foo", "bar", "asdf")

  print(a)

}

The compiler states following error:

[ERROR]   implicit def AtoB: A => B = a => B(a.a, a.b, a.c, a.foobar)
[ERROR]                                                       ^
[ERROR] one error found

However, the following code is not running into a compiler error, even if x is not a member of a case class:

package Hokuspokus

object ImplicitMagic extends App {

  case class A(a: String, b: String, c: String)
  case class B(d: String, e: String, f: String, x: String)

  implicit def AtoB: A => B = a => B(a.a, a.b, a.c, a.x)

  def print(b: B): Unit = {
    System.out.println("Print" + b.d)
  }

  val a = A("foo", "bar", "asdf")

  print(a)

}

I am wondering now, why the scala compiler is not detecting this issue during compile time. In order to understand what the scalac compiler does, I investigated the compiled scala classes, but so far I did not come up with a conclusion.

package Hokuspokus
object ImplicitMagic extends scala.AnyRef with scala.App {
  def this() = { /* compiled code */ }
  case class A(a: scala.Predef.String, b: scala.Predef.String, c: scala.Predef.String) extends scala.AnyRef with scala.Product with scala.Serializable {
    val a: scala.Predef.String = { /* compiled code */ }
    val b: scala.Predef.String = { /* compiled code */ }
    val c: scala.Predef.String = { /* compiled code */ }
    def copy(a: scala.Predef.String, b: scala.Predef.String, c: scala.Predef.String): Hokuspokus.ImplicitMagic.A = { /* compiled code */ }
    override def productPrefix: java.lang.String = { /* compiled code */ }
    def productArity: scala.Int = { /* compiled code */ }
    def productElement(x$1: scala.Int): scala.Any = { /* compiled code */ }
    override def productIterator: scala.collection.Iterator[scala.Any] = { /* compiled code */ }
    def canEqual(x$1: scala.Any): scala.Boolean = { /* compiled code */ }
    override def productElementName(x$1: scala.Int): java.lang.String = { /* compiled code */ }
    override def hashCode(): scala.Int = { /* compiled code */ }
    override def toString(): java.lang.String = { /* compiled code */ }
    override def equals(x$1: scala.Any): scala.Boolean = { /* compiled code */ }
  }
  object A extends scala.runtime.AbstractFunction3[scala.Predef.String, scala.Predef.String, scala.Predef.String, Hokuspokus.ImplicitMagic.A] with java.io.Serializable {
    def this() = { /* compiled code */ }
    final override def toString(): java.lang.String = { /* compiled code */ }
    def apply(a: scala.Predef.String, b: scala.Predef.String, c: scala.Predef.String): Hokuspokus.ImplicitMagic.A = { /* compiled code */ }
    def unapply(x$0: Hokuspokus.ImplicitMagic.A): scala.Option[scala.Tuple3[scala.Predef.String, scala.Predef.String, scala.Predef.String]] = { /* compiled code */ }
  }
  case class B(d: scala.Predef.String, e: scala.Predef.String, f: scala.Predef.String, x: scala.Predef.String) extends scala.AnyRef with scala.Product with scala.Serializable {
    val d: scala.Predef.String = { /* compiled code */ }
    val e: scala.Predef.String = { /* compiled code */ }
    val f: scala.Predef.String = { /* compiled code */ }
    val x: scala.Predef.String = { /* compiled code */ }
    def copy(d: scala.Predef.String, e: scala.Predef.String, f: scala.Predef.String, x: scala.Predef.String): Hokuspokus.ImplicitMagic.B = { /* compiled code */ }
    override def productPrefix: java.lang.String = { /* compiled code */ }
    def productArity: scala.Int = { /* compiled code */ }
    def productElement(x$1: scala.Int): scala.Any = { /* compiled code */ }
    override def productIterator: scala.collection.Iterator[scala.Any] = { /* compiled code */ }
    def canEqual(x$1: scala.Any): scala.Boolean = { /* compiled code */ }
    override def productElementName(x$1: scala.Int): java.lang.String = { /* compiled code */ }
    override def hashCode(): scala.Int = { /* compiled code */ }
    override def toString(): java.lang.String = { /* compiled code */ }
    override def equals(x$1: scala.Any): scala.Boolean = { /* compiled code */ }
  }
  object B extends scala.runtime.AbstractFunction4[scala.Predef.String, scala.Predef.String, scala.Predef.String, scala.Predef.String, Hokuspokus.ImplicitMagic.B] with java.io.Serializable {
    def this() = { /* compiled code */ }
    final override def toString(): java.lang.String = { /* compiled code */ }
    def apply(d: scala.Predef.String, e: scala.Predef.String, f: scala.Predef.String, x: scala.Predef.String): Hokuspokus.ImplicitMagic.B = { /* compiled code */ }
    def unapply(x$0: Hokuspokus.ImplicitMagic.B): scala.Option[scala.Tuple4[scala.Predef.String, scala.Predef.String, scala.Predef.String, scala.Predef.String]] = { /* compiled code */ }
  }
  implicit def AtoB: scala.Function1[Hokuspokus.ImplicitMagic.A, Hokuspokus.ImplicitMagic.B] = { /* compiled code */ }
  def print(b: Hokuspokus.ImplicitMagic.B): scala.Unit = { /* compiled code */ }
  val a: Hokuspokus.ImplicitMagic.A = { /* compiled code */ }
}

Solution

  • Compiler is doing multiple things to resolve a missing method/val a.foobar.

    It will check if this method belongs to case class A, it will check if A can be implicitly converted to a different type that contains method foobar or if there is an implicit class that adds method foobar.

    Eventually, it decides that this method is not available and thus you see the compiler error.

    In case when you use a.x, compiler does find an implicit conversion from A to B that provides method/val x. Unfortunately, it does not catch the fact that this happens in the actual conversion. What compiler is doing in this case is following

    implicit def AtoB: A => B = a => B(a.a, a.b, a.c, AtoB(a).x)
    

    This does compile, but will produce StackOveflowException in runtime.