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.