Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

scala

slick

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]                                                                ^
like image 874
SkyWalker Avatar asked Dec 06 '16 12:12

SkyWalker


1 Answers

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.

like image 70
Jasper-M Avatar answered Sep 28 '22 16:09

Jasper-M