嵌套案例类之间的递归转换,其中目标中的字段是源类的未对齐子集

llo*_*eta 5 scala generic-programming typeclass type-level-computation shapeless

给定一对case类,Source并且Target,它们具有嵌套的case类,并且在每个嵌套级别,其中的字段Target都是未对齐的子集Source,是否有办法将通用的无Shape转换变换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
  )
}
Run Code Online (Sandbox Code Playgroud)

我希望能够做类似的事情

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)
Run Code Online (Sandbox Code Playgroud)

我有一些代码负责选择子集并对齐字段,但递归部分更具挑战性:

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]
}
Run Code Online (Sandbox Code Playgroud)

我已经看过这个问题了,但是那里的答案没有考虑到这些字段是另一个字段的未对齐子集的事实.

Mil*_*bin 7

这是一个有趣的练习,将各种原始图形拼凑在一起以获得结果.以下已经使用Scala 2.12.6和2.13.0-M5进行了无形2.3.3的测试...

我们可以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)))))
      }
    }
}
Run Code Online (Sandbox Code Playgroud)

有趣的案例是transformGen.该类型的变量TU是源和目标类型和固定在呼叫站点.其余的类型变量按顺序从左到右求解,因为隐式参数从上到下被解析...在大多数情况下,给定前面的类型参数解析每个隐式的最终类型参数,并且解决方案流向右/直到后来的决议.

还要注意使用无形的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
  )
)
Run Code Online (Sandbox Code Playgroud)

以下按预期工作,

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)
Run Code Online (Sandbox Code Playgroud)