Странный NPE с io.circe.Decoder - PullRequest
6 голосов
/ 23 мая 2019

У меня есть 2 переменные, объявленные следующим образом,

 implicit val decodeURL: Decoder[URL] = Decoder.decodeString.emapTry(s => Try(new URL(s)))     // #1

 implicit val decodeCompleted = Decoder[List[URL]].prepare(_.downField("completed")) // #2

Обе строки компилируются и запускаются.

Однако, если я аннотирую # 2 с типом, т.е. implicit val decodeCompleted: Decoder[List[URL]] = Decoder[List[URL]].prepare(_.downField("completed")). Он компилируется и # 2 будет выбрасывать NullPointerException (NPE) во время выполнения.

Как это могло произойти? Я не знаю, является ли это Цирцеей или просто проблемой Скалы. Почему № 2 отличается от № 1? Спасибо

Ответы [ 2 ]

3 голосов
/ 23 мая 2019

Проблема в том, что вы должны использовать импликации с аннотациями всегда.

Теперь, когда вы используете их, когда они не аннотированы, вы попадаете в некую неопределенную / недопустимую зону поведения. Вот почему с аннотированным случается что-то вроде этого:

  • Я хочу использовать Decoder[List[URL]]
  • нет (аннотированный) Decoder[List[URL]] неявный в области действия
  • давайте выведем его как обычно (не нужно generic.auto._, потому что определение для этого есть в сопутствующем объекте)
  • после получения вы вызываете его .prepare (_. DownField ("выполнено"))
  • конечный результат имеет тип Decoder[List[URL]], так что выводится тип decodeCompleted

Теперь, что произойдет, если вы аннотируете?

  • Я хочу использовать Decoder[List[URL]]
  • существует decodeCompleted, объявленный как нечто, удовлетворяющее этому определению
  • пусть использовать decodeCompleted значение
  • но decodeCompleted не инициализирован! фактически мы инициализируем его прямо сейчас!
  • в результате вы получите decodeCompleted = null

Это практически равно:

val decodeCompleted = decodeCompleted

за исключением того, что слой косвенности становится способом обнаружения абсурда этого компилятором. (Если вы замените val на def, вы получите бесконечную рекурсию и переполнение стека):

@ implicit val s: String = implicitly[String] 
s: String = null

@ implicit def s: String = implicitly[String] 
defined function s

@ s 
java.lang.StackOverflowError
  ammonite.$sess.cmd1$.s(cmd1.sc:1)
  ammonite.$sess.cmd1$.s(cmd1.sc:1)
  ammonite.$sess.cmd1$.s(cmd1.sc:1)
  ammonite.$sess.cmd1$.s(cmd1.sc:1)

Да, это запутано компилятором. Вы не сделали ничего плохого, и в идеальном мире это сработало бы.

Сообщество Scala смягчает это, выделяя:

  • автоопределение - когда вам нужно где-то неявное и оно автоматически выводится без определения для него переменной
  • полуавтоматическое деривация - когда вы производите в значение и делаете это значение неявным

В последнем случае у вас обычно есть некоторые утилиты, такие как:

import io.circe.generic.semiauto._

implicit val decodeCompleted: Decoder[List[URL]] = deriveDecoder[List[URL]]

Это работает, потому что принимает неявное DerivedDecoder[A], а затем извлекает из него Decoder[A], поэтому вы никогда не получите сценарий implicit val a: A = implicitly[A].

1 голос
/ 23 мая 2019

На самом деле проблема в том, что вы вводите рекурсивный val, как объяснил @MateuszKubuszok.

Самый простой, хотя и немного уродливый, обходной путь:

implicit val decodeCompleted: Decoder[List[URL]] = {
  val decodeCompleted = null
  Decoder[List[URL]].prepare(_.downField("completed"))
}

Путем затенения decodeCompleted в правой части неявный поиск больше не будет рассматривать его как кандидата внутри этого кодового блока, поскольку на него больше нельзя ссылаться.

...