Преобразование вложенных классов case в вложенные Карты с использованием Shapeless

Я пытаюсь решить этот вопрос, используя Shapeless, вкратце о преобразовании вложенного класса case в Map [String, Any], вот пример:

case class Person(name:String, address:Address)
case class Address(street:String, zip:Int)

val p = Person("Tom", Address("Jefferson st", 10000))

Он хочет преобразовать p в следующее:

Map("name" -> "Tom", "address" -> Map("street" -> "Jefferson st", "zip" -> 10000))

Я пытаюсь сделать это с помощью Shapeless LabelledGeneric, вот что я до сих пор:

import shapeless._
import record._, syntax.singleton._
import ops.record._
import shapeless.ops.record._

def writer[T,A<:HList,H<:HList](t:T)
(implicit lGeneric:LabelledGeneric.Aux[T,A],
 kys:Keys.Aux[A,H],
 vls:Values[A]) = {
    val tGen = lGeneric.to(t)
    val keys = Keys[lGeneric.Repr].apply
    val values = Values[lGeneric.Repr].apply(tGen)
    println(keys)
    println(values)
  }

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

values.map(identity)
//or
tGen.map(identity)

Error:(75, 19) could not find implicit value for parameter mapper: shapeless.ops.hlist.FlatMapper[shapeless.poly.identity.type,vls.Out]
    values.flatMap(identity)
                  ^
Error:(75, 19) not enough arguments for method flatMap: (implicit mapper: shapeless.ops.hlist.FlatMapper[shapeless.poly.identity.type,vls.Out])mapper.Out.
Unspecified value parameter mapper.
    values.flatMap(identity)
                  ^

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

Ответ 1

В любое время, когда вы хотите выполнить операцию типа flatMap в HList, тип которой не статически известен, вам необходимо предоставить доказательства (в виде неявного параметра), что операция действительно доступна для этого типа. Вот почему компилятор жалуется на отсутствие экземпляров FlatMapper - он не знает, как flatMap(identity) над произвольным HList без них.

Чистым способом выполнения такого рода действий будет определение класса пользовательского типа. Shapeless уже предоставляет класс ToMap для записей, и мы можем считать его отправной точкой, хотя он не обеспечивает точно то, что вы ищете ( он не работает рекурсивно на вложенных классах классов).

Мы можем написать примерно следующее:

import shapeless._, labelled.FieldType, record._

trait ToMapRec[L <: HList] { def apply(l: L): Map[String, Any] }

Теперь нам нужно предоставить экземпляры для трех случаев. Первый случай - это базовый регистр - пустая запись, а ниже - hnilToMapRec.

Второй случай - это случай, когда мы знаем, как преобразовать хвост записи, и мы знаем, что голова - это то, что мы также можем рекурсивно преобразовать (hconsToMapRec0 здесь).

Последний случай похож, но для головок, у которых нет экземпляров ToMapRec (hconsToMapRec1). Обратите внимание, что нам нужно использовать признак LowPriority, чтобы убедиться, что этот экземпляр имеет приоритет по отношению к hconsToMapRec0, если бы мы этого не сделали, у обоих будет одинаковый приоритет, и мы получим ошибки в неоднозначных экземплярах.

trait LowPriorityToMapRec {
  implicit def hconsToMapRec1[K <: Symbol, V, T <: HList](implicit
    wit: Witness.Aux[K],
    tmrT: ToMapRec[T]
  ): ToMapRec[FieldType[K, V] :: T] = new ToMapRec[FieldType[K, V] :: T] {
    def apply(l: FieldType[K, V] :: T): Map[String, Any] =
      tmrT(l.tail) + (wit.value.name -> l.head)
  }
}

object ToMapRec extends LowPriorityToMapRec {
  implicit val hnilToMapRec: ToMapRec[HNil] = new ToMapRec[HNil] {
    def apply(l: HNil): Map[String, Any] = Map.empty
  }

  implicit def hconsToMapRec0[K <: Symbol, V, R <: HList, T <: HList](implicit
    wit: Witness.Aux[K],
    gen: LabelledGeneric.Aux[V, R],
    tmrH: ToMapRec[R],
    tmrT: ToMapRec[T]
  ): ToMapRec[FieldType[K, V] :: T] = new ToMapRec[FieldType[K, V] :: T] {
    def apply(l: FieldType[K, V] :: T): Map[String, Any] =
      tmrT(l.tail) + (wit.value.name -> tmrH(gen.to(l.head)))
  }
}

Наконец, мы предлагаем некоторый синтаксис для удобства:

implicit class ToMapRecOps[A](val a: A) extends AnyVal {
  def toMapRec[L <: HList](implicit
    gen: LabelledGeneric.Aux[A, L],
    tmr: ToMapRec[L]
  ): Map[String, Any] = tmr(gen.to(a))
}

И тогда мы можем продемонстрировать, что он работает:

scala> p.toMapRec
res0: Map[String,Any] = Map(address -> Map(zip -> 10000, street -> Jefferson st), name -> Tom)

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

Ответ 2

У меня проблема с подходом, предоставленным Трэвисом Брауном.
Некоторые классы классов вложенности не преобразуются в Map https://scalafiddle.io/sf/cia2jTa/0.

Ответ был найден здесь.
Чтобы исправить решение, просто обведите ToMapRec [T] в неявные параметры Lazy [ToMapRec [T]]. Исправлена ​​скрипка https://scalafiddle.io/sf/cia2jTa/1