Scala: how to define an abstract rewritable superclass for any case class?

Please carry me, there is some context as long as the OP does not make sense. I am using Slick 3.1.x and the smooth code generator. btw All source code can be found in play-authenticate-use- scala github project . For this project, I would like to have a smooth overall Dao to avoid repeating the same template code for each model.

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

Then I call the generator that generates the following data model: Tables.scala

To provide generic dao slick implementations for model classes, I need them to conform to some elementary abstractions, for example.

  • Entity trait : every object has an id , for example. necessary for dao findById
  • The AutoIncEntity property declares a def copyWithNewId(id : PK) : Entity[PK] method. This is necessary to implement dao createAndFetch , which saves a new object and retrieves the automatically generated PK id in one step.

This copyWithNewId is an OP point. Note that it is called copyWithNewId , not copy , to avoid infinite recursion. In order to be able to implement GenericDaoAutoIncImpl , which allows you to insert and immediately retrieve the automatically generated id , the entity string requires the copy(id = id) method based on the case <Model>Row class, which is not yet known at the definition point of GenericDaoAutoIncImpl . The corresponding implementation is as follows:

 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 generated AutoInc id model, and this is not nice, for example.

 // 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 a subclass of the case <Model>Row class of the Base class, which can be copied and copied, except for the passed id ie IdCopyable with copy(id = id) , then I would not have to implement again and again this copyWithNewId for each generated case <Model>Row class.

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

UPDATE 1 The following is the problem:

 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 lens 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) } } 

Then I 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] ^ 
+6
source share
1 answer

With shapeless, you can abstract over case classes.

1. Manual abstraction by class classes

If you assume that each id is 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) 

This may be possible in a cleaner way; I am not very good at formless programming yet. And I'm pretty sure that it is also possible to do this for case classes with any id type at any position in the parameter list. See paragraph 2 below.


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

 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 dash that your case classes can extend, if that matters 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) 

The important thing is that you distribute the typeclass instance from the site for use where necessary, using context boundaries 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 just that purpose. Thus, 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)) 

Note that, as in the assignNewIds method, where copyWithId used, an instance of typeclass IdCopy[A,ID] requested as an implicit parameter. This is because copyWithId requires the implicit instance of IdCopy[A,ID] be in scope when it is used. You need to distribute implicit instances from the application site where you work with specific types, such as Foo , right up to the call to the chain where copyWithId is copyWithId .

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

+8
source

Source: https://habr.com/ru/post/1012942/


All Articles