Рекурсивное преобразование между вложенными классами случаев, когда поля в целевом объекте являются невыровненными подмножествами исходного класса.

Учитывая пару классов case, Source и Target, которые имеют вложенные классы case, и на каждом уровне вложенности поля в Target являются невыровненными подмножествами полей в Source, есть ли способ написать универсальное преобразование на основе Shapeless из Source до Target?

Например, для следующих классов Internal и External:

object Internal {
  case class User(
    firstName: String,
    lastName: String,
    isAdmin: Boolean,
    address: Address
  )

  case class Address(
    street: String,
    country: String,
    blacklisted: Boolean
  )
}

object External {
  // Note that isAdmin is missing and the fields are jumbled
  case class User(
    lastName: String,
    firstName: String,
    address: Address
  )

  // blacklisted is gone
  case class Address(
    street: String,
    country: String
  )
}

Я хотел бы иметь возможность сделать что-то вроде

val internalUser = Internal.User(
  firstName = "Joe",
  lastName = "Blow",
  isAdmin = false,
  address = Internal.Address(
    street = "Sesame",
    country = "U-S-A",
    blacklisted = false
  )
)

val externalUser = Transform.into[External.User](internalUser)

У меня есть какой-то код, который заботится о выборе подмножества и выравнивании полей, но часть рекурсии немного сложнее:

import shapeless._, ops.hlist.Align, ops.hlist.SelectAll, SelectAll._

class Transform[T] {

  // The fun stuff. Given an S, returns a T, if S has the right (subset of) fields
  def apply[S, SR <: HList, TR <: HList](s: S)(
      implicit
      genS: LabelledGeneric.Aux[S, SR],
      genT: LabelledGeneric.Aux[T, TR],
      selectAll: SelectAll[SR, TR],
      align: Align[SelectAll[SR, TR]#Out, TR]): T =
    genT.from(align(selectAll(genS.to(s))))
}

object Transform {

  // Convenience method for building an instance of `Transform`
  def into[T] = new Transform[T]
}

Я рассмотрел этот вопрос SO, но ответ не учитывает тот факт, что поля являются невыровненными подмножествами другого.


person lloydmeta    schedule 23.08.2018    source источник
comment
Я знаю, что вопрос касается бесформенности, поэтому просто комментарий: вы можете посмотреть на scalalandio.github.io/ дымоход, если вы еще этого не сделали.   -  person Krever    schedule 23.08.2018
comment
@Кревер, это круто; не знал об этом :) спасибо   -  person lloydmeta    schedule 23.08.2018


Ответы (1)


Это было забавным упражнением по сборке различных примитивов в бесформенном виде, чтобы получить результат. Следующее было протестировано с shapeless 2.3.3 со Scala 2.12.6 и 2.13.0-M5...

Мы можем определить класс типа Transform следующим образом:

import shapeless._, ops.hlist.ZipWithKeys, ops.record.{ Keys, SelectAll, Values }

trait Transform[T, U] {
  def apply(t: T): U
}

object Transform {
  def into[U] = new MkTransform[U]
  class MkTransform[U] {
    def apply[T](t: T)(implicit tt: Transform[T, U]): U = tt(t)
  }

  // The identity transform
  implicit def transformId[T]: Transform[T, T] =
    new Transform[T, T] {
      def apply(t: T): T = t
    }

  // Transform for HLists
  implicit def transformHCons[H1, T1 <: HList, H2, T2 <: HList]
    (implicit
      th: Transform[H1, H2],
      tt: Transform[T1, T2]
    ): Transform[H1 :: T1, H2 :: T2] =
    new Transform[H1 :: T1, H2 :: T2] {
      def apply(r: H1 :: T1): H2 :: T2 = th(r.head) :: tt(r.tail)
    }

  // Transform for types which have a LabelledGeneric representation as
  // a shapeless record
  implicit def transformGen
    [T, U, TR <: HList, UR <: HList, UK <: HList, UV <: HList, TS <: HList]
    (implicit
      genT:    LabelledGeneric.Aux[T, TR],  // T <-> corresponding record
      genU:    LabelledGeneric.Aux[U, UR],  // U <-> corresponding record
      keysU:   Keys.Aux[UR, UK],            // Keys of the record for U
      valuesU: Values.Aux[UR, UV],          // Values of the record for U
      selT:    SelectAll.Aux[TR, UK, TS],   // Select the values of the record of T
                                            //   corresponding to the keys of U
      trans:   Lazy[Transform[TS, UV]],     // Transform the selected values
      zipKeys: ZipWithKeys.Aux[UK, UV, UR], // Construct a new record of U from the
                                            //   transformed values
    ): Transform[T, U] =
    new Transform[T, U] {
      def apply(t: T): U = {
        genU.from(zipKeys(trans.value(selT(genT.to(t)))))
      }
    }
}

Интересный случай transformGen. Переменные типа T и U являются исходным и целевым типами и фиксируются на сайтах вызовов. Остальные переменные типа решаются последовательно, слева направо, поскольку неявные аргументы разрешаются сверху вниз... в большинстве случаев последний аргумент типа каждого неявного типа решается с учетом предыдущих аргументов типа, и решение течет вправо/ вплоть до последующих резолюций.

Обратите также внимание на использование shapeless Lazy для защиты рекурсивного неявного аргумента trans. Это не обязательно для вашего примера, но может быть в более сложных или рекурсивных случаях. Также обратите внимание, что в Scala 2.13.0-M5 и более поздних версиях trans вместо этого может быть определено как неявный аргумент по имени.

Теперь, учитывая ваши определения,

val internalUser = Internal.User(
  firstName = "Joe",
  lastName = "Blow",
  isAdmin = false,
  address = Internal.Address(
    street = "Sesame",
    country = "U-S-A",
    blacklisted = false
  )
)

следующие работы по желанию,

val expectedExternalUser = External.User(
  lastName = "Blow",
  firstName = "Joe",
  address = External.Address(
    street = "Sesame",
    country = "U-S-A",
  )
)

val externalUser = Transform.into[External.User](internalUser)

assert(externalUser == expectedExternalUser)
person Miles Sabin    schedule 23.08.2018