Search code examples
scalaimplicit-conversiontypeclasscircescanamo

Stack overflow in typeclass with implicit conversion


I made a generic DynamoFormat for Scanamo that would put any object that has Circe's Encoder and Decoder defined into database as a Json string.

import com.gu.scanamo.DynamoFormat
import io.circe.parser.parse
import io.circe.syntax._
import io.circe.{Decoder, Encoder}

object JsonDynamoFormat {    
  def forType[T: Encoder: Decoder]: DynamoFormat[T] = DynamoFormat.coercedXmap[T, String, Exception] {
    s => parse(s).flatMap(_.as[T]).fold(err => throw err, obj => obj)
  } {
    obj => obj.asJson.noSpaces
  }
}

Then I added an implicit conversion (to the same object JsonDynamoFormat) to automatically provide these formatters.

implicit def jsonToFormat[T: Encoder: Decoder]: DynamoFormat[T] = JsonDynamoFormat.forType[T]

When I import it, compiler resolves formatters successfully, however at runtime I get a stack overflow in JsonDynamoFormat, where calls to jsonToFormat and forType alternate infinitely:

Exception in thread "main" java.lang.StackOverflowError
    at JsonDynamoFormat$.forType(JsonDynamoFormat.scala:12)
    at JsonDynamoFormat$.jsonToFormat(JsonDynamoFormat.scala:9)
    at JsonDynamoFormat$.forType(JsonDynamoFormat.scala:13)
    at JsonDynamoFormat$.jsonToFormat(JsonDynamoFormat.scala:9)
    ...

I can't really understand what happens here. Could anyone shed a light on this?


Solution

  • Debugging Scala implicits errors can be quite taxing. Here is a couple of suggestions that can help:

    • Enable scalacOptions ++= Seq("-Xlog-implicits") compiler option. This will print implicit search log, and can be useful to understand where exactly the implicit chain breaks.

    • Add splain libraryDependencies ++= Seq(compilerPlugin("io.tryp" %% "splain" % "0.2.4")) to improve the implicit debug log readability.

    In general, stack overflow at runtime with generically derived typeclasses is a sign of wrong implicit resolution. This usually means compiler has found a couple of circularly dependent implicits and used one of them to satisfy the other one, and vice versa.

    Normally such situation is recognized at compile time, and compile produces "diverging implicits" error, but that error can be a false positive, and therefore library authors usually circumvent it by using a technique like Lazy typeclass from shapeless. However in case of an actual buggy circular implicits, this will result in runtime error, instead of compile time error.