I am working on a side project, trying to implement immutable aggregates in Scala. My idea is to have base trait AggregateRoot
with some common behaviors. Child classes would be actual aggregates modeled as a case classes. For now, there is one thing that I don't like, and that is that I cannot call copy
method from the base trait for two reasons:
copy
method inside of the base traitcopy
method will haveI have some basic knowledge of shapeless library and I thought that it might help in this case. My idea is pass list of tagged fields to the base method in the trait which will replace them and return new instance of the case class. As a step in that direction I am trying to create method that will copy one single field for the beginning using shapeless, but I am getting the same error all the time, that compiler cannot find the implicit for updater.
Here is the simplified code fragment that I am trying to use:
import shapeless._
import shapeless.labelled.FieldType
import shapeless.ops.record.Updater
import shapeless.record._
import shapeless.syntax.singleton._
trait AggregateRoot[T <: AggregateRoot[T]] {
self: T =>
def makeCopy[L <: HList, K, V](ft: FieldType[K, V])(
implicit
labeledGen: LabelledGeneric.Aux[T, L],
updater: Updater.Aux[L, FieldType[K, V], L],
witness: Witness.Aux[K]): T = {
val labeledHList = labeledGen.to(this)
val result = labeledHList.updated(witness, ft)
labeledGen.from(result)
}
}
case class User(id: String, age: Int) extends AggregateRoot[User]()
val user1 = User("123", 10)
val ageChange = "age" ->> 22
val user2 = user1.makeCopy(ageChange)
Since I am not experienced shapeless user, I am not sure why it cannot find requested implicit. Version of shapeless is 2.3.3.
As far as I understood from this great answer: How to generically update a case class field using LabelledGeneric? - you can't have Updater in general case, because Shapeless need to derive it for each particular field in case class, which means instead of having general purpose makeCopy
you will need to have method for each particular field, like:
import shapeless._, record._, ops.record._, labelled._, syntax.singleton._, tag._
trait AggregateRoot[T <: AggregateRoot[T]] {
self: T =>
import AggregateRoot._
def withAge[L <: HList, K, V](age: Int)(implicit
gen: LabelledGeneric.Aux[T, L],
upd: Updater.Aux[L, F, L]
): T = {
val ageField = AgeField(age)
gen.from(upd(gen.to(this), ageField))
}
}
object AggregateRoot {
type AgeField = Symbol with Tagged[Witness.`"age"`.T]
val AgeField = field[AgeField]
type F = FieldType[AgeField, Int]
}
import AggregateRoot._
case class User(id: String, age: Int) extends AggregateRoot[User]
object User {
implicit val gen = LabelledGeneric[User]
}
val user1 = User("123", 10)
val ageChange = "age" ->> 22
val user2 = user1.withAge(ageChange)
val test = User("test-id", 20)
println("test.withAge(42) = " + test.withAge(20))
println("test.withAge(12).withAge(42) = " + test.withAge(12).withAge(42))
Scatie: https://scastie.scala-lang.org/kDnL6HTQSEeSqVduW3EOnQ