通用实体记录 scala - 引入无形状的 id 字段

Generic entity records scala - introduce id field with shapeless

问题: 用 ID 字段描述记录(使其成为实体) ID 字段将需要自动生成,以便 记录(也就是A)+ID=实体(也就是B)

trait Record extends Product
trait Entity {
  type Id
}
case class Book(title: String, author: String, publication: Int)
case class PersistentBook(id: Long, title: String, author: String, publication: Int) extends Entity {
  type Id = Long
}

object PersistentRecords {

  def main(args: Array[String]): Unit = {
    val bookGen = Generic[Book]
    val persistentBookGen = Generic[PersistentBook]

    val scalaBook = Book("Programming in Scala", "Odersky, Spoon, Venners", 2008)


    val scalaBookHlist = bookGen.to(scalaBook)

    val persistentScalaBookHList = 15L :: scalaBookHlist
    val persistentScalaBookFromGeneric: PersistentBook = persistentBookGen.from(persistentScalaBookHList)
    println(s"Book: $scalaBook")
    println(s"PBook: $persistentScalaBookFromGeneric")

    val genHListScalaBook = injectFieldSimpleGeneric(scalaBook, 15L)
    println(s"GenBook: $genHListScalaBook")

    val persistedScalaBook = injectFieldGeneric(scalaBook, 16L)
    println(s"PersistedBook: $persistedScalaBook")
  }

  // OK
  def injectField[F](baseRecord: HList, field: F): HList =
    field :: baseRecord

  // OK
  def injectFieldSimpleGeneric[A, ARepr <: HList, F](baseRecord: A, field: F)(implicit aGen: LabelledGeneric.Aux[A, ARepr]): HList = {
    val baseHList = aGen.to(baseRecord)
    val compositeHList: HList = field :: baseHList
    compositeHList
  }


  def injectFieldGeneric[A, ARepr <: HList, B <: Entity, BRepr <: HList, F <: Entity#Id ](baseRecord: A, idField: F)(
    implicit aGen: LabelledGeneric.Aux[A, ARepr],
             bGen: LabelledGeneric.Aux[B, BRepr]): B = {
    val baseHList = aGen.to(baseRecord)
    val compositeHList  = idField :: baseHList
    bGen.from(compositeHList) //Type mismatch. Required BRepr, found F :: ARepr
  }
}

输出:

Book: Book(Programming in Scala,Odersky, Spoon, Venners,2008)

PBook: PersistentBook(15,Programming in Scala,Odersky, Spoon, Venners,2008)

GenBook: 15 :: Programming in Scala :: Odersky, Spoon, Venners :: 2008 :: HNil

到目前为止我得到的最接近的是 injectFieldSimpleGeneric,但它 returns 是一个 HList,而不是 B objective 是为了能够为记录生成 ID,以便我可以使用自己生成的 ID 插入它们 当我尝试扩展它以生成 B 时,B 的 HList 不兼容

这里有两个问题。首先是您没有向编译器提供 AReprBRepr 与某些共享结构相关的任何证据。您可以通过更改 bGen 约束来做到这一点:

import shapeless._, shapeless.labelled.{FieldType, field}

trait Record extends Product
trait Entity { type Id }
case class Book(title: String, author: String, publication: Int)
case class PersistentBook(id: Long, title: String, author: String, publication: Int) extends
  Entity { type Id = Long }

def injectFieldGeneric[A, ARepr <: HList, B <: Entity, F <: B#Id](baseRecord: A, idField: F)(
  implicit aGen: LabelledGeneric.Aux[A, ARepr],
           bGen: LabelledGeneric.Aux[B, FieldType[Witness.`'id`.T, F] :: ARepr]
  ): B = {
    val baseHList = aGen.to(baseRecord)
    val compositeHList  = field[Witness.`'id`.T](idField) :: baseHList
    bGen.from(compositeHList)
  }

这个有效:

val bookGen = LabelledGeneric[Book]
val scalaBook = Book("Programming in Scala", "Odersky, Spoon, Venners", 2008)
val persistedScalaBook =
  injectFieldGeneric[Book, bookGen.Repr, PersistentBook, Long](scalaBook, 16L)

然后:

scala> println(persistedScalaBook)
PersistentBook(16,Programming in Scala,Odersky, Spoon, Venners,2008)

不幸的是,您绝对不希望每次调用此方法时都必须提供所有类型参数,而编译器无法推断它们:

scala> val persistedScalaBook = injectFieldGeneric(scalaBook, 16L)
                                ^
       error: inferred type arguments [Book,Nothing,Nothing,Long] do not conform to method injectFieldGeneric's type parameter bounds [A,ARepr <: shapeless.HList,B <: Entity,F <: B#Id]
                                                   ^
       error: type mismatch;
        found   : Book
        required: A
                                                              ^
       error: type mismatch;
        found   : Long(16L)
        required: F
                                                  ^
       error: could not find implicit value for parameter aGen: shapeless.LabelledGeneric.Aux[A,ARepr]

问题是,即使您已经向编译器提供了 AB 共享结构的证据,您还没有告诉它如何选择 BB 没有出现在此处显式参数中的任何地方,并且编译器不会枚举范围内的所有情况 classes 试图找到一个具有适当 LabelledGeneric 实例的情况。

有两种方法可以解决此问题。一种是像这样的 class 类型:

trait HasEntity[A] { type E }
object HasEntity { type Aux[A, E0] = HasEntity[A] { type E = E0 } }

然后为每对案例 classes 提供类似 HasEntity.Aux[Book, PersistentBook] 的实例。另一种方法是重写 injectFieldGeneric 以便您可以提供单个类型参数:

class PartiallyAppliedInject[B <: Entity] {
  type IdK = Witness.`'id`.T

  def apply[A, ARepr <: HList, F <: B#Id, BRepr <: HList](baseRecord: A, idField: F)(
    implicit aGen: LabelledGeneric.Aux[A, ARepr],
             bGen: LabelledGeneric.Aux[B, FieldType[IdK, F] :: ARepr]
    ): B = {
      val baseHList = aGen.to(baseRecord)
      val compositeHList  = field[IdK](idField) :: baseHList
      bGen.from(compositeHList)
    }
}

def injectFieldGeneric[B <: Entity]: PartiallyAppliedInject[B] =
  new PartiallyAppliedInject[B]

然后:

scala> val persistedScalaBook = injectFieldGeneric[PersistentBook](scalaBook, 16L)
persistedScalaBook: PersistentBook = PersistentBook(16,Programming in Scala,Odersky, Spoon, Venners,2008)

在这里您仍然需要指定目标,但编译器将能够验证它是否有效匹配并将所需的映射放在一起。