Scala:如何为任何案例类定义一个抽象的可复制超类? [英] Scala: how to define an abstract copyable superclass for any case class?

查看:44
本文介绍了Scala:如何为任何案例类定义一个抽象的可复制超类?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

请耐心等待,在 OP 有意义之前有一些上下文.我正在使用 Slick 3.1.x 和 Slick 代码生成器.btw 整个源代码可以在 play-authenticate-usage-scala github 项目中找到.对于这个项目,我想要一个灵活的通用 Dao,以避免为每个模型重复相同的样板代码.

我有一个 postgres sql 脚本,它在这里使用进化创建数据库:1.sql

然后我调用一个生成器来生成以下数据模型:Tables.scala >

为了能够为模型类提供通用的 dao slick 实现,我需要它们遵守一些基本的抽象,例如

  • 实体 trait:每个实体都有一个 id 例如需要 dao 的 findById
  • AutoIncEntity trait 声明方法 def copyWithNewId(id : PK) : Entity[PK].这是 dao 的 createAndFetch 实现所需要的,该实现将一个新实体持久化并在一个步骤中检索自动生成的 id PK.

这个 copyWithNewId 是 OP 的重点.请注意,它被称为 copyWithNewId 而不是 copy 以避免无限递归.为了能够实现 GenericDaoAutoIncImpl 允许插入并立即获取自动生成的 id,实体行需要来自 copy(id = id) 方法;Row 案例类,在定义 GenericDaoAutoIncImpl 时尚不知道.相关实现如下:

override def createAndFetch(entity: E): Future[Option[E]] = {val insertQuery = tableQuery 返回 tableQuery.map(_.id)进入 ((row, id) => row.copyWithNewId(id))db.run((insertQuery += entity).flatMap(row => findById(row.id)))}

这要求我在每个 AutoInc id 生成的模型中实现 copyWithNewId 方法,这并不好,例如

//生成的代码,稍后修改以适应通用 daocase class UserRow(id: Long, ...) 使用 Subject { 扩展 AutoIncEntity[Long]覆盖 def copyWithNewId(id : Long) : Entity[Long] = this.copy(id = id)}

但是,如果我可以 - 使用一些 Scala 技巧 - 定义我的 <Model>Row 案例类子类,该类是可复制的,除了传递的 idIdCopyablecopy(id = id) 那么我就不需要为每个 一遍又一遍地实现这个 copyWithNewId生成的案例类.

对于包含 id 属性的任何 case 类,有没有办法抽象或上拉"重构 copy(id = id) ?还有其他推荐的解决方案吗?

更新 1 以下几乎总结了我遇到的问题:

scala>抽象类 BaseA[A <: BaseA[_]] { def copy(id : Int) : A }定义类 BaseA标度>case class A(id: Int) extends BaseA[A]<console>:12: 错误:类 A 需要是抽象的,因为类型 (id: Int)A 的类 BaseA 中的方法副本未定义case class A(id: Int) extends BaseA[A]^标度>案例类 A(id: Int);val a = A(5);a.copy(6)定义类 Aa: A = A(5)res0: A = A(6)

UPDATE 2 使用下面建议的解决方案,我得到以下编译错误:

[error]/home/bravegag/code/play-authenticate-usage-scala/app/dao/GenericDaoAutoIncImpl.scala:26: 找不到参数 gen 的隐式值:shapeless.Generic.Aux[E,Repr][错误] val insertQuery = tableQuery 将 tableQuery.map(_.id) 返回到 ((row, id) => row.copyWithNewId(id))[错误]^[错误]/home/bravegag/code/play-authenticate-usage-scala/app/dao/GenericDaoAutoIncImpl.scala:27​​: 值 id 不是 insertQuery.SingleInsertResult 的成员[错误] db.run((insertQuery += entity).flatMap(row => findById(row.id)))[错误]^[错误] 发现两个错误

更新 3 使用和调整下面建议的镜头解决方案,我收到以下编译器错误:

import shapeless._, tag.@@进口无形._导入标签.$at$at/*** 所有强实体模型类型的可识别基础* @tparam PK 主键类型* @tparam E 实际案例类 EntityRow 类型*/trait AutoIncEntity[PK, E <: AutoIncEntity[PK, E]] extends Entity[PK] { self: E =>//------------------------------------------------------------------------//上市//------------------------------------------------------------------------/*** 返回具有数据库生成的更新 id 的实体* @param id 实体id* @return 由数据库生成的具有更新 id 的实体*/def copyWithNewId(id : PK)(隐式 mkLens: MkFieldLens.Aux[E, Symbol @@ Witness.`"id"`.T, PK]) : E = {(lens[E] >> 'id).set(self)(id)}}

然后我收到以下编译器错误:

[error]/home/bravegag/code/play-authenticate-usage-scala/app/dao/GenericDaoAutoIncImpl.scala:26: 找不到参数 mkLens 的隐式值: shapeless.MkFieldLens.Aux[E,shapeless.tag.@@[Symbol,String("id")],PK][错误] val insertQuery = tableQuery 将 tableQuery.map(_.id) 返回到 ((row, id) => row.copyWithNewId(id))[错误]^[错误]/home/bravegag/code/play-authenticate-usage-scala/app/dao/GenericDaoAutoIncImpl.scala:27​​: 值 id 不是 insertQuery.SingleInsertResult 的成员[错误] db.run((insertQuery += entity).flatMap(row => findById(row.id)))[错误]^

解决方案

使用 shapeless 你可以抽象案例类.

1.手动抽象案例类

如果你假设每个 id 都是一个 Long 并且是 case 类的第一个参数,它可能看起来像这样:

scala>导入 shapeless._, ops.hlist.{IsHCons, Prepend}进口无形._导入 ops.hlist.{IsHCons, Prepend}标度>trait Copy[A <: Copy[A]] { self: A =>|def copyWithId[Repr <: HList, Tail <: HList](l: Long)(|隐含的|gen: Generic.Aux[A,Repr],|缺点:IsHCons.Aux[Repr,Long,Tail],|prep: Prepend.Aux[Long :: HNil,Tail,Repr]|) = gen.from(prep(l :: HNil, cons.tail(gen.to(self))))|}定义性状副本标度>case class Foo(id: Long, s: String) extends Copy[Foo]定义类 Foo标度>Foo(4L, "foo").copyWithId(5L)res1: Foo = Foo(5,foo)

也有可能以更简洁的方式进行;我还不是很精通无形编程.而且我漂亮确信也可以对参数列表中任何位置具有任何类型的 id 的案例类执行此操作.请参阅下面的第 2 段.

<小时>

您可能希望将此逻辑封装在可重用的类型类中:

scala>:粘贴//进入粘贴模式(ctrl-D 完成)导入 shapeless._, ops.hlist.{IsHCons, Prepend}密封性状 IdCopy[A] {def copyWithId(self: A, id: Long): A}对象 IdCopy {def apply[A: IdCopy] = 隐式[IdCopy[A]]隐式 def mkIdCopy[A, Repr <: HList, Tail <: HList](隐含的gen: Generic.Aux[A,Repr],缺点:IsHCons.Aux[Repr,Long,Tail],prep: Prepend.Aux[Long :: HNil,Tail,Repr]): IdCopy[A] =新的 IdCopy[A] {def copyWithId(self: A, id: Long): A =gen.from(prep(id :: HNil, cons.tail(gen.to(self))))}}//退出粘贴模式,现在解释.进口无形._导入 ops.hlist.{IsHCons, Prepend}定义的特征 IdCopy定义的对象 IdCopy标度>def copy[A: IdCopy](a: A, id: Long) = IdCopy[A].copyWithId(a, id)copy: [A](a: A, id: Long)(隐性证据$1: IdCopy[A])A标度>case class Foo(id: Long, str: String)定义类 Foo标度>复制(富(4L,富"),5L)res0: Foo = Foo(5,foo)

如果这对您很重要,您仍然可以将 copyWithId 方法放在您的案例类可以扩展的特征中:

scala>trait Copy[A <: Copy[A]] { self: A =>|def copyWithId(id: Long)(隐式复制:IdCopy[A]) = copy.copyWithId(self, id)|}定义性状副本标度>case class Foo(id: Long, str: String) extends Copy[Foo]定义类 Foo标度>Foo(4L, "foo").copyWithId(5L)res1: Foo = Foo(5,foo)

重要的是,通过使用上下文边界或隐式参数,将类型类实例从使用站点传播到需要它的地方.

override def createAndFetch(entity: E)(implicit copy: IdCopy[E]): Future[Option[E]] = {val insertQuery = tableQuery 返回 tableQuery.map(_.id)进入 ((row, id) => row.copyWithId(id))db.run((insertQuery += entity).flatMap(row => findById(row.id)))}

2.使用镜头

Shapeless 还提供 镜头,您可以将其用于此目的.这样您就可以更新任何具有某些 id 字段的案例类的 id 字段.

scala>:粘贴//进入粘贴模式(ctrl-D 完成)密封性状 IdCopy[A,ID] {def copyWithId(self: A, id: ID): A}对象 IdCopy {导入无形._,标签.@@隐式 def mkIdCopy[A, ID](隐含的mkLens: MkFieldLens.Aux[A, Symbol @@Witness.`"id"`.T, ID]): IdCopy[A,ID] =新的 IdCopy[A,ID] {def copyWithId(self: A, id: ID): A =(lens[A] >> 'id).set(self)(id)}}def copyWithId[ID, A](a: A, elem: ID)(隐式复制: IdCopy[A,ID]) = copy.copyWithId(a, elem)//退出粘贴模式,现在解释.定义的特征 IdCopy定义的对象 IdCopycopyWithId: [ID, A](a: A, elem: ID)(隐式复制: IdCopy[A,ID])A标度>trait Entity[ID] { def id: ID }定义特征实体标度>case class Foo(id: String) 扩展 Entity[String]定义类 Foo标度>def assignNewIds[ID, A <: Entity[ID]](entities: List[A], ids: List[ID])(implicit copy: IdCopy[A,ID]): List[A] =|entity.zip(ids).map{ case (entity, id) =>copyWithId(entity, id) }assignNewIds: [ID, A <: Entity[ID]](entities: List[A], ids: List[ID])(隐式复制:IdCopy[A,ID])List[A]标度>assignNewIds( List(Foo("foo"),Foo("bar")), List("new1", "new2"))res0: List[Foo] = List(Foo(new1), Foo(new2))

注意如何在使用 copyWithId 的方法 assignNewIds 中,请求类型类 IdCopy[A,ID] 的实例为一个隐式参数.这是因为 copyWithId 在使用时需要 IdCopy[A,ID] 的隐式实例在范围内.您需要从使用站点传播隐式实例,在那里您使用诸如 Foo 之类的具体类型,一直沿着调用链向下传递到调用 copyWithId 的位置.

您可以将隐式参数视为方法的依赖项.如果方法具有 IdCopy[A,ID] 类型的隐式参数,则在调用它时需要满足该依赖项.通常,这也会对调用方法的方法产生相同的依赖性.

Please bear with me, there is some context until the OP makes sense. I'm using Slick 3.1.x and the slick code generator. btw The whole source code can be found in the play-authenticate-usage-scala github project. For this project I'd like to have a slick generic Dao to avoid repeating the same boilerplate code for every model.

I have a postgres sql script that creates the database using evolutions here: 1.sql

I then invoke a generator that generates the following data model: Tables.scala

To be able to provide generic dao slick implementations for the model classes I need them to comply to some basic abstractions e.g.

  • Entity trait: Every entity has an id e.g. needed for dao's findById
  • AutoIncEntity trait declares the method def copyWithNewId(id : PK) : Entity[PK]. This is needed for the dao's implementation of createAndFetchthat persists a new entity and retrieves the auto generated id PK in one step.

This copyWithNewId is the point of the OP. Note that it is called copyWithNewId and not copy to avoid infinite recursion. To be able to implement the GenericDaoAutoIncImpl that allows inserting and immediately fetching the auto generated id, the entity row requires a copy(id = id) method coming from the <Model>Row case class that at the point of defining the GenericDaoAutoIncImpl it is not yet known. The relevant implementation is the following:

override def createAndFetch(entity: E): Future[Option[E]] = {
  val insertQuery = tableQuery returning tableQuery.map(_.id) 
                        into ((row, id) => row.copyWithNewId(id))
  db.run((insertQuery += entity).flatMap(row => findById(row.id)))
}

And this requires me to implement the copyWithNewId method in every AutoInc id generated model and that is not nice e.g.

// generated code and modified later to adapt it for the generic dao 
case class UserRow(id: Long, ...) extends AutoIncEntity[Long] with Subject {
  override def copyWithNewId(id : Long) : Entity[Long] = this.copy(id = id)
}

However if I could - using some Scala trick - define my <Model>Row case classes subclass of a Base class that is copyable and copies itself except for the passed idi.e. IdCopyable with copy(id = id) then I would not need to implement over and over this copyWithNewId for every <Model>Row generated case class.

Is there a way to abstract or "pull up" refactor copy(id = id) for any case class that contains an id attribute? is there any other recommended solution?

UPDATE 1 The following pretty much summarizes the problem I have:

scala> abstract class BaseA[A <: BaseA[_]] { def copy(id : Int) : A }
defined class BaseA

scala> case class A(id: Int) extends BaseA[A]
<console>:12: error: class A needs to be abstract, since method copy in class BaseA of type (id: Int)A is not defined
   case class A(id: Int) extends BaseA[A]
              ^

scala> case class A(id: Int); val a = A(5); a.copy(6)
defined class A
a: A = A(5)
res0: A = A(6)

UPDATE 2 Using the proposed solution below I get the following compilation errors:

[error] /home/bravegag/code/play-authenticate-usage-scala/app/dao/GenericDaoAutoIncImpl.scala:26: could not find implicit value for parameter gen: shapeless.Generic.Aux[E,Repr]
[error]     val insertQuery = tableQuery returning tableQuery.map(_.id) into ((row, id) => row.copyWithNewId(id))
[error]                                                                                                     ^
[error] /home/bravegag/code/play-authenticate-usage-scala/app/dao/GenericDaoAutoIncImpl.scala:27: value id is not a member of insertQuery.SingleInsertResult
[error]     db.run((insertQuery += entity).flatMap(row => findById(row.id)))
[error]                                                                ^
[error] two errors found

UPDATE 3 using and adapting the proposed lenses solution below I get the following compiler errors:

import shapeless._, tag.@@
import shapeless._
import tag.$at$at

/**
  * Identifyable base for all Strong Entity Model types
  * @tparam PK Primary key type
  * @tparam E Actual case class EntityRow type
  */
trait AutoIncEntity[PK, E <: AutoIncEntity[PK, E]] extends Entity[PK] { self: E =>
  //------------------------------------------------------------------------
  // public
  //------------------------------------------------------------------------
  /**
    * Returns the entity with updated id as generated by the database
    * @param id The entity id
    * @return the entity with updated id as generated by the database
    */
  def copyWithNewId(id : PK)(implicit mkLens: MkFieldLens.Aux[E, Symbol @@ Witness.`"id"`.T, PK]) : E = {
    (lens[E] >> 'id).set(self)(id)
  }
}

I then get the following compiler error:

[error] /home/bravegag/code/play-authenticate-usage-scala/app/dao/GenericDaoAutoIncImpl.scala:26: could not find implicit value for parameter mkLens: shapeless.MkFieldLens.Aux[E,shapeless.tag.@@[Symbol,String("id")],PK]
[error]     val insertQuery = tableQuery returning tableQuery.map(_.id) into ((row, id) => row.copyWithNewId(id))
[error]                                                                                                     ^
[error] /home/bravegag/code/play-authenticate-usage-scala/app/dao/GenericDaoAutoIncImpl.scala:27: value id is not a member of insertQuery.SingleInsertResult
[error]     db.run((insertQuery += entity).flatMap(row => findById(row.id)))
[error]                                                                ^

解决方案

With shapeless you can abstract over case classes.

1. Manually abstracting over case classes

If you assume every id is a Long and is the first parameter of the case class, it might look like this:

scala> import shapeless._, ops.hlist.{IsHCons, Prepend}
import shapeless._
import ops.hlist.{IsHCons, Prepend}

scala> trait Copy[A <: Copy[A]] { self: A =>
     |   def copyWithId[Repr <: HList, Tail <: HList](l: Long)(
     |     implicit 
     |     gen: Generic.Aux[A,Repr], 
     |     cons: IsHCons.Aux[Repr,Long,Tail], 
     |     prep: Prepend.Aux[Long :: HNil,Tail,Repr]
     |   ) = gen.from(prep(l :: HNil, cons.tail(gen.to(self))))
     | }
defined trait Copy

scala> case class Foo(id: Long, s: String) extends Copy[Foo]
defined class Foo

scala> Foo(4L, "foo").copyWithId(5L)
res1: Foo = Foo(5,foo)

It might also be possible in a cleaner way; I'm not very proficient at shapeless programming yet. And I'm pretty sure it's also possible to do it for case classes with any type of id in any position in the parameter list. See paragraph 2 below.


You might want to encapsulate this logic in a reusable typeclass:

scala> :paste
// Entering paste mode (ctrl-D to finish)

import shapeless._, ops.hlist.{IsHCons, Prepend}

sealed trait IdCopy[A] {
  def copyWithId(self: A, id: Long): A
}

object IdCopy {
  def apply[A: IdCopy] = implicitly[IdCopy[A]]
  implicit def mkIdCopy[A, Repr <: HList, Tail <: HList](
    implicit 
    gen: Generic.Aux[A,Repr], 
    cons: IsHCons.Aux[Repr,Long,Tail], 
    prep: Prepend.Aux[Long :: HNil,Tail,Repr]
  ): IdCopy[A] = 
    new IdCopy[A] {
      def copyWithId(self: A, id: Long): A = 
        gen.from(prep(id :: HNil, cons.tail(gen.to(self))))
    }
}

// Exiting paste mode, now interpreting.

import shapeless._
import ops.hlist.{IsHCons, Prepend}
defined trait IdCopy
defined object IdCopy

scala> def copy[A: IdCopy](a: A, id: Long) = IdCopy[A].copyWithId(a, id)
copy: [A](a: A, id: Long)(implicit evidence$1: IdCopy[A])A

scala> case class Foo(id: Long, str: String)
defined class Foo

scala> copy(Foo(4L, "foo"), 5L)
res0: Foo = Foo(5,foo)

You can still put your copyWithId method in a trait that your case classes can extend, if that's important to you:

scala> trait Copy[A <: Copy[A]] { self: A =>
     |   def copyWithId(id: Long)(implicit copy: IdCopy[A]) = copy.copyWithId(self, id)
     | }
defined trait Copy

scala> case class Foo(id: Long, str: String) extends Copy[Foo]
defined class Foo

scala> Foo(4L, "foo").copyWithId(5L)
res1: Foo = Foo(5,foo)

What's important is that you propagate the typeclass instance from the use site to where it is needed, through the use of context bounds or implicit parameters.

override def createAndFetch(entity: E)(implicit copy: IdCopy[E]): Future[Option[E]] = {
  val insertQuery = tableQuery returning tableQuery.map(_.id) 
                        into ((row, id) => row.copyWithId(id))
  db.run((insertQuery += entity).flatMap(row => findById(row.id)))
}

2. Using lenses

Shapeless also provides lenses that you can use for exactly this purpose. That way you can update the id field of any case class that has some id field.

scala> :paste
// Entering paste mode (ctrl-D to finish)

sealed trait IdCopy[A,ID] {
  def copyWithId(self: A, id: ID): A
}

object IdCopy {
  import shapeless._, tag.@@
  implicit def mkIdCopy[A, ID](
    implicit 
    mkLens: MkFieldLens.Aux[A, Symbol @@ Witness.`"id"`.T, ID]
  ): IdCopy[A,ID] = 
    new IdCopy[A,ID] {
      def copyWithId(self: A, id: ID): A = 
        (lens[A] >> 'id).set(self)(id)
    }
}


def copyWithId[ID, A](a: A, elem: ID)(implicit copy: IdCopy[A,ID]) = copy.copyWithId(a, elem)

// Exiting paste mode, now interpreting.

defined trait IdCopy
defined object IdCopy
copyWithId: [ID, A](a: A, elem: ID)(implicit copy: IdCopy[A,ID])A

scala> trait Entity[ID] { def id: ID }
defined trait Entity

scala> case class Foo(id: String) extends Entity[String]
defined class Foo

scala> def assignNewIds[ID, A <: Entity[ID]](entities: List[A], ids: List[ID])(implicit copy: IdCopy[A,ID]): List[A] =
     |   entities.zip(ids).map{ case (entity, id) =>  copyWithId(entity, id) }
assignNewIds: [ID, A <: Entity[ID]](entities: List[A], ids: List[ID])(implicit copy: IdCopy[A,ID])List[A]

scala> assignNewIds( List(Foo("foo"),Foo("bar")), List("new1", "new2"))
res0: List[Foo] = List(Foo(new1), Foo(new2))

Notice how also in the method assignNewIds where copyWithId is used, an instance of the typeclass IdCopy[A,ID] is requested as an implicit parameter. This is because copyWithId requires an implicit instance of IdCopy[A,ID] to be in scope when it is used. You need to propagate the implicit instances from the use site, where you work with concrete types such as Foo, all the way down the call chain to where copyWithId is called.

You can view implicit parameters as the dependencies of a method. If a method has an implicit parameter of type IdCopy[A,ID], you need to satisfy that dependency when you call it. Often that also puts that same dependency on the method from where it is called.

这篇关于Scala:如何为任何案例类定义一个抽象的可复制超类?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆