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.