嵌套大小写 class 之间的递归转换,其中目标中的字段是源 class 的未对齐子集

Recursive transformation between nested case classes where the fields in the target are unaligned subsets of the source class

给定一对大小写 类、SourceTarget,它们具有嵌套的大小写 类,并且在每个嵌套级别,TargetSource 中子集的未对齐子集,有没有一种方法可以编写从 SourceTarget 的通用无形变换?

例如,给定以下 InternalExternal 类:

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)

我有 some code 负责选择子集和对齐字段,但递归部分更具挑战性:

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]
}

我已经查看了 ,但那里的答案并没有考虑到字段是另一个字段未对齐的子集这一事实。

这是一个有趣的练习,将 shapeless 中的各种图元拼凑在一起以获得结果。以下内容已经过 shapeless 2.3.3 与 Scala 2.12.6 和 2.13.0-M5 的测试...

我们可以像这样定义一个Transform类型class,

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。类型变量 TU 是源类型和目标类型,在调用点是固定的。剩余的类型变量按顺序从左到右求解,因为隐式参数从上到下解析......在大多数情况下,每个隐式的最终类型参数在给定前面的类型参数的情况下求解,并且解决方案流动right/down后续决议。

还要注意使用 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)