9

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]                                                                ^
SkyWalker
  • 13,729
  • 18
  • 91
  • 187

1 Answers1

12

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.

Jasper-M
  • 14,966
  • 2
  • 26
  • 37
  • Thank you!! I'm happy if it works but ... man that trait's code is awful :D – SkyWalker Dec 06 '16 at 16:36
  • 1
    Consider a typeclass instead - https://tpolecat.github.io/2015/04/29/f-bounds.html – Reactormonk Dec 06 '16 at 16:45
  • I thought about it but typeclass at the end create wrappers this might turn to be a performance flaw for a data driven Web App. Model wrapper instances going outbound of the dao is OK but not sure if it is when they come inbound into the DAO. – SkyWalker Dec 06 '16 at 16:50
  • I get the compiler error `could not find implicit value for parameter gen: shapeless.Generic.Aux[E,Repr]` see the updated question. – SkyWalker Dec 06 '16 at 17:32
  • This is awesome! Thanks heaps. – Paul Dolega Dec 06 '16 at 18:26
  • What happens if I prefer not to lock the `id` to be Long? but rather a PK template parameter? does it work still? – SkyWalker Dec 08 '16 at 12:35
  • @Reactormonk it would be nicer to have a typeclass answer rather than a link to a generic description that doesn't really answer the OP. Giving links like that is not really the spirit of SO, the devil is in the details. Besides, I find the typeclass explanation in that blog really terrible. – SkyWalker Dec 08 '16 at 13:02
  • The lenses solution doesn't seem to work, what Scala are you using? I am using 2.11.8 ... I'd rather be happy with a more portable (and understandable) typeclass solution even if I have to type a bit more code. – SkyWalker Dec 08 '16 at 13:21
  • `You will need to propagate the right implicits to where they are needed, and you might want to make your code cleaner by factoring this MkFieldLens logic out to a custom typeclass.` this sounds like a riddle deserving another SO question. The lens solution would be fine if I didn't have to solve all the riddles. – SkyWalker Dec 08 '16 at 13:43
  • 1
    @GiovanniAzua that's why it's a comment, not an answer. – Reactormonk Dec 08 '16 at 14:11
  • @GiovanniAzua I updated the answer and tried to be more detailed. I tried it in scala 2.12.0 and 2.11.8. Currently you're pretty much stuck with fairly complex shapeless solutions if you want to really abstract over case classes. Otherwise copy pasting something like `def copyWithId(id: Long) = copy(id = id)` into every case class is about the best you're going to get. – Jasper-M Dec 08 '16 at 14:35
  • @Jasper-M did you see the **UPDATE 3** compiler errors I got above trying to adapt the lenses solution? – SkyWalker Dec 08 '16 at 14:44
  • @GiovanniAzua Yes, that's probably caused by not having an implicit parameter in the method that calls `copyWithNewId`. And I very strongly recommend using the `IdCopy[A,ID]` typeclass instead, because I noticed some weird compilation problems myself when using the bare `MkFieldLens` typeclass. – Jasper-M Dec 08 '16 at 14:53