Ответ 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 находятся в списке, кортеже и т.д., но вы можете довольно легко распространить его на эти случаи.