1
votes

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:

  1. I don't have access to copy method inside of the base trait
  2. I have no idea how many parameters copy method will have

I 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.

1
I would say it would be actually easier, simpler and better to just add tour own set of withX methods that can be used instead of copyLuis Miguel Mejía Suárez

1 Answers

0
votes

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