Странное поведение, пытающееся преобразовать классы case в гетерогенные списки рекурсивно с помощью Shapeless

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

В этой минимизированной версии я просто определяю класс типа, который будет рекурсивно преобразовывать классы case в разнородные списки:

import shapeless._

trait DeepHLister[R <: HList] extends DepFn1[R] { type Out <: HList }

trait LowPriorityDeepHLister {
  type Aux[R <: HList, Out0 <: HList] = DeepHLister[R] { type Out = Out0 }

  implicit def headNotCaseClassDeepHLister[H, T <: HList](implicit
    dht: DeepHLister[T]
  ): Aux[H :: T, H :: dht.Out] = new DeepHLister[H :: T] {
    type Out = H :: dht.Out
    def apply(r: H :: T) = r.head :: dht(r.tail)
  }
}

object DeepHLister extends LowPriorityDeepHLister {
  implicit object hnilDeepHLister extends DeepHLister[HNil] {
    type Out = HNil
    def apply(r: HNil) = HNil
  }

  implicit def headCaseClassDeepHLister[H, R <: HList, T <: HList](implicit
    gen: Generic.Aux[H, R],
    dhh: DeepHLister[R],
    dht: DeepHLister[T]
  ): Aux[H :: T, dhh.Out :: dht.Out] = new DeepHLister[H :: T] {
    type Out = dhh.Out :: dht.Out
    def apply(r: H :: T) = dhh(gen.to(r.head)) :: dht(r.tail)
  }

  def apply[R <: HList](implicit dh: DeepHLister[R]): Aux[R, dh.Out] = dh
}

Попробуй! Сначала нам нужны некоторые классы классов:

case class A(x: Int, y: String)
case class B(x: A, y: A)
case class C(b: B, a: A)
case class D(a: A, b: B)

И затем (обратите внимание, что я очистил синтаксис типа, чтобы это не было абсолютно нечитаемым беспорядком):

scala> DeepHLister[A :: HNil]
res0: DeepHLister[A :: HNil]{
  type Out = (Int :: String :: HNil) :: HNil
} = [email protected]

scala> DeepHLister[B :: HNil]
res1: DeepHLister[B :: HNil] {
  type Out = (
    (Int :: String :: HNil) :: (Int :: String :: HNil) :: HNil
  ) :: HNil
} = [email protected]

scala> DeepHLister[C :: HNil]
res2: DeepHLister[C :: HNil] {
  type Out = (
    ((Int :: String :: HNil) :: (Int :: String :: HNil) :: HNil) ::
    (Int :: String :: HNil) ::
    HNil
  ) :: HNil
} = [email protected]

Пока все хорошо. Но тогда:

scala> DeepHLister[D :: HNil]
res3: DeepHLister[D :: HNil] {
  type Out = ((Int :: String :: HNil) :: B :: HNil) :: HNil
} = [email protected]

Не удалось преобразовать B. Если мы включим -Xlog-implicits, это будет последнее сообщение:

<console>:25: this.DeepHLister.headCaseClassDeepHLister is not a valid implicit value for DeepHLister[shapeless.::[B,shapeless.HNil]] because:
hasMatchingSymbol reported error: diverging implicit expansion for type DeepHLister[this.Repr]
starting with method headNotCaseClassDeepHLister in trait LowPriorityDeepHLister
              DeepHLister[D :: HNil]
                         ^

Для меня это не имеет смысла. headCaseClassDeepHLister должен иметь возможность генерировать DeepHLister[B :: HNil] просто отлично, и это происходит, если вы спросите его напрямую.

Это происходит как в версиях 2.10.4, так и в версии 2.11.2, и с версией 2.0.0 и с мастером. Я почти уверен, что это ошибка, но я не исключаю, что я делаю что-то неправильно. Кто-нибудь видел что-то подобное раньше? Что-то не так с моей логикой или каким-то ограничением на Generic Мне не хватает?

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

Ответ 1

Теперь это работает более или менее, как написано, используя недавние сборки без формального-2.1.0-SNAPSHOT, и близкий родственник образца в этом вопросе был добавлен там как example.

Проблема с оригиналом заключается в том, что каждое расширение a Generic вводит новый тип HList в неявное разрешение экземпляров класса DeepHLister и, в принципе, может генерировать тип HList, который связанных, но более сложных, чем некоторый тип, который был замечен ранее в течение той же резолюции. Это условие отключает проверку расхождения и прерывает процесс разрешения.

Точные сведения о том, почему это происходит для D, но не для C, скрываются в деталях реализации Scala typechecker, но в грубом приближении отличием является то, что во время разрешения для C мы видим B (больше) до A (меньше), поэтому проверяющая расходимость счастлива, что наши типы сходятся; наоборот, во время разрешения для D мы видим A (меньше) до B (больше), поэтому проверка отклонения (консервативно) сворачивается.

Исправление для этого в бесформенном 2.1.0 - это недавно расширенный конструктор типа Lazy и связанная неявная инфраструктура макросов. Это позволяет гораздо большему пользователю контролировать расхождение и поддерживает использование неявного разрешения для построения рекурсивных неявных значений, которые имеют решающее значение для способности автоматически выводить экземпляры класса типа для рекурсивных типов. Многие примеры этого можно найти в бесформенной базе кода, в частности, в инфраструктуре деривации класса переработанного типа и реализации Scrap Your Boilerplate, которые больше не требуют выделенной поддержки макросов, но полностью реализованы в терминах Generic и Lazy примитивы. Различные применения этих механизмов можно найти в подпроекте бесформенных примеров.

Ответ 2

Я принял несколько иной подход.

trait CaseClassToHList[X] {
  type Out <: HList
}

trait LowerPriorityCaseClassToHList {
  implicit def caseClass[X](implicit gen: Generic[X]): CaseClassToHList[X] {
    type Out = generic.Repr
  } = null
}

object CaseClassToHList extends LowerPriorityCaseClassToHList {
  type Aux[X, R <: HList] = CaseClassToHList[X] { type Out = R }

  implicit def caseClassWithCaseClasses[X, R <: HList](
    implicit toHList: CaseClassToHList.Aux[X, R],
    nested: DeepHLister[R]): CaseClassToHList[X] {
    type Out = nested.Out
  } = null
}

trait DeepHLister[R <: HList] {
  type Out <: HList
}

object DeepHLister {

  implicit def hnil: DeepHLister[HNil] { type Out = HNil } = null

  implicit def caseClassAtHead[H, T <: HList](
    implicit head: CaseClassToHList[H],
    tail: DeepHLister[T]): DeepHLister[H :: T] {
    type Out = head.Out :: tail.Out
  } = null

  def apply[X <: HList](implicit d: DeepHLister[X]): d.type = null
}

Протестировано следующим кодом:

case class A(x: Int, y: String)
case class B(x: A, y: A)
case class C(b: B, a: A)
case class D(a: A, b: B)

object Test {

  val z = DeepHLister[HNil]
  val typedZ: DeepHLister[HNil] {
    type Out = HNil
  } = z

  val a = DeepHLister[A :: HNil]
  val typedA: DeepHLister[A :: HNil] {
    type Out = (Int :: String :: HNil) :: HNil
  } = a

  val b = DeepHLister[B :: HNil]
  val typedB: DeepHLister[B :: HNil] {
    type Out = ((Int :: String :: HNil) :: (Int :: String :: HNil) :: HNil) :: HNil
  } = b

  val c = DeepHLister[C :: HNil]
  val typedC: DeepHLister[C :: HNil] {
    type Out = (((Int :: String :: HNil) :: (Int :: String :: HNil) :: HNil) :: (Int :: String :: HNil) :: HNil) :: HNil 
  } = c

  val d = DeepHLister[D :: HNil]
  val typedD: DeepHLister[D :: HNil] {
    type Out = ((Int :: String :: HNil) :: ((Int :: String :: HNil) :: (Int :: String :: HNil) :: HNil) :: HNil) :: HNil
  } = d
}