Переполнение стека в классе типов с неявным преобразованием

Я сделал общий DynamoFormat для Scanamo, который поместил бы любой объект, который имеет Encoder и Decoder Circe, определенные в базу данных, как строку Json.

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
  }
}

Затем я добавил неявное преобразование (в тот же object JsonDynamoFormat) для автоматического предоставления этих форматтеров.

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

Когда я его импортирую, компилятор успешно разрешает средства форматирования, однако во время выполнения я получаю переполнение стека в JsonDynamoFormat, где вызовы jsonToFormat и forType чередуются бесконечно:

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

Я не могу понять, что здесь происходит. Может ли кто-нибудь пролить свет на это?


person Vasiliy Ivashin    schedule 25.08.2017    source источник
comment
Интересно, что произойдет, если вы сделаете def forType неявным и удалите implicit def jsonToFormat. Похоже, что второй из них в значительной степени лишний.   -  person Haspemulator    schedule 25.08.2017
comment
@Haspemulator, что интересно, он не компилируется, хотя сигнатуры функций действительно кажутся одинаковыми. Странный.   -  person Vasiliy Ivashin    schedule 25.08.2017
comment
Я думаю, что этот сбой является ключом к тому, чтобы добраться до первопричины проблемы. Включите scalacOptions ++= Seq("-Xlog-implicits"), чтобы получить журнал неявного разрешения (может быть много выходных данных), и дополнительно libraryDependencies ++= Seq(compilerPlugin("io.tryp" %% "splain" % "0.2.4")), чтобы сделать этот вывод более приятным. Это может помочь в объяснении, почему неявное не найдено.   -  person Haspemulator    schedule 25.08.2017
comment
Похоже, компилятор решил удовлетворить один неявный параметр функции другим значением, отсюда циклическая зависимость и переполнение стека. Вы можете попробовать поместить эти две функции в разные области, чтобы только одна видела другую.   -  person Haspemulator    schedule 25.08.2017
comment
Спасибо за предложения. Изучу это и отпишусь.   -  person Vasiliy Ivashin    schedule 25.08.2017
comment
Преобразование forType в implicit def не удалось скомпилировать из-за конфликта имени с методом, импортированным из другого класса. После разрешения конфликта имен произошел сбой во время выполнения с бесконечной рекурсией :) Это привело меня к поиску других имплицитов, и я обнаружил, что coercedXmap принимает неявный параметр типа DynamoFormat[String], который, конечно же, разрешается в forType. После предоставления правильного средства форматирования строк все работает так, как ожидалось. Еще раз спасибо за ваши ценные предложения относительно неявной отладки. Если вы хотите предоставить их в качестве ответа, я буду рад принять его.   -  person Vasiliy Ivashin    schedule 28.08.2017


Ответы (1)


Отладка неявных ошибок Scala может быть весьма утомительной. Вот пара советов, которые могут помочь:

  • Включите параметр компилятора scalacOptions ++= Seq("-Xlog-implicits"). Это распечатает журнал неявного поиска и может быть полезно понять, где именно разрывается неявная цепочка.

  • Добавьте splain libraryDependencies ++= Seq(compilerPlugin("io.tryp" %% "splain" % "0.2.4")), чтобы улучшить читабельность неявного журнала отладки.

Как правило, переполнение стека во время выполнения с классами типов, производными от общего, является признаком неправильного неявного разрешения. Обычно это означает, что компилятор нашел пару циклически зависимых имплицитов и использовал одно из них для удовлетворения другого, и наоборот.

Обычно такая ситуация распознается во время компиляции, и компиляция выдает ошибку «расходящиеся имплициты», но эта ошибка может быть ложным срабатыванием, и поэтому авторы библиотек обычно обходят ее, используя технику типа Lazy typeclass from shapeless. Однако в случае фактических циклических имплицитов с ошибками это приведет к ошибке времени выполнения, а не к ошибке времени компиляции.

person Haspemulator    schedule 28.08.2017