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

标签 scala typeclass generic-programming shapeless type-level-computation

给定一对具有嵌套案例类的案例类,SourceTarget,并且在每个嵌套级别,Target 中的字段code> 是 Source 中子集的未对齐子集,有没有办法编写从 SourceTarget 的通用 Shapeless 转换?

例如,给定以下 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]
}

我查看了this SO question ,但答案没有考虑到这些字段是另一个字段的未对齐子集这一事实。

最佳答案

这是一个有趣的练习,将各种无形状的基元拼凑在一起以获得结果。以下内容已使用 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。类型变量 TU 是源类型和目标类型,并且在调用站点处固定。其余类型变量按从左到右的顺序求解,因为隐式参数从上到下解析...在大多数情况下,每个隐式的最终类型参数在给定前面的类型参数的情况下得到求解,并且解决方案向右流动/直至后续决议。

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

关于scala - 嵌套案例类之间的递归转换,其中目标中的字段是源类的未对齐子集,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51979764/

相关文章:

scala - 在 Slick 3 中的多种方法之间共享数据库 session

scala - Spark独立模式: How to compress spark output written to HDFS

haskell - 如何不对使用更通用函数的受限函数应用实例约束?

haskell - 在 Haskell 中对结构相似类型内的值进行操作

haskell - 在 SYB 中匹配更高种类的类型

scala - Spark sql窗口功能滞后

scala - 如何在不创建中间序列的情况下对 Iterable 进行排序?

typeclass - Rust:使用特征/类型类来实现通用数字函数

c++ - 为通用树定义迭代器

使用模板和 nullptr 的 C++ 泛型编程