Similar to this case class question but with a twist:
I have a case class which has some deeply nested case classes as properties. As a simple example,
case class Foo(fooPropA:Option[String], fooPropB:Option[Int])
case class Bar(barPropA:String, barPropB:Int)
case class FooBar(name:Option[String], foo:Foo, optionFoo: Option[Foo], bar:Option[Bar])
I'd like to merge two FooBar case classes together, taking the values which exist for an input and applying them to an existing instance, producing an updated version:
val fb1 = FooBar(Some("one"), Foo(Some("propA"), None), Some(Foo(Some("propA"), Some(3))), Some(Bar("propA", 4)))
val fb2 = FooBar(None, Foo(Some("updated"), Some(2)), Some(Foo(Some("baz"), None)), None)
val merged = fb1.merge(fb2)
//merged = FooBar(Some("one"), Foo(Some("updated"), Some(2)), Some(Foo(Some("baz"), Some(3))), Some(Bar("propA", 4)))
I know I can use a lens to compose the deeply nested property updates; however, I feel this will require a lot of boiler plate code: I need a lens for every property, and another composed lens in the parent class. This seems like a lot to maintain, even if using the more succinct lens creation approach in shapeless.
The tricky part is the optionFoo element: in this scenario, both elements exist with a Some(value). However, I'd like to merge the inner-option properties, not just overwrite fb1 with fb2's new values.
I'm wondering if there is a good approach to merge these two values together in a way which requires minimal code. My gut feeling tells me to try to use the unapply
method on the case class to return a tuple, iterate over and combine the tuples into a new tuple, and then apply the tuple back to a case class.
Is there a more efficient way to go about doing this?
My previous answer used Shapeless 1.2.4, Scalaz, and shapeless-contrib, and Shapeless 1.2.4 and shapeless-contrib are pretty outdated at this point (over two years later), so here's an updated answer using Shapeless 2.2.5 and cats 0.3.0. I'll assume a build configuration like this:
scalaVersion := "2.11.7"
libraryDependencies ++= Seq(
"com.chuusai" %% "shapeless" % "2.2.5",
"org.spire-math" %% "cats" % "0.3.0"
)
Shapeless now includes a ProductTypeClass
type class that we can use here. Eventually Miles Sabin's kittens project (or something similar) is likely to provide this kind of thing for cats's type classes (similar to the role that shapeless-contrib played for Scalaz), but for now just using ProductTypeClass
isn't too bad:
import algebra.Monoid, cats.std.all._, shapeless._
object caseClassMonoids extends ProductTypeClassCompanion[Monoid] {
object typeClass extends ProductTypeClass[Monoid] {
def product[H, T <: HList](ch: Monoid[H], ct: Monoid[T]): Monoid[H :: T] =
new Monoid[H :: T] {
def empty: H :: T = ch.empty :: ct.empty
def combine(x: H :: T, y: H :: T): H :: T =
ch.combine(x.head, y.head) :: ct.combine(x.tail, y.tail)
}
val emptyProduct: Monoid[HNil] = new Monoid[HNil] {
def empty: HNil = HNil
def combine(x: HNil, y: HNil): HNil = HNil
}
def project[F, G](inst: => Monoid[G], to: F => G, from: G => F): Monoid[F] =
new Monoid[F] {
def empty: F = from(inst.empty)
def combine(x: F, y: F): F = from(inst.combine(to(x), to(y)))
}
}
}
And then:
import cats.syntax.semigroup._
import caseClassMonoids._
case class Foo(fooPropA: Option[String], fooPropB: Option[Int])
case class Bar(barPropA: String, barPropB: Int)
case class FooBar(name: Option[String], foo: Foo, bar: Option[Bar])
And finally:
scala> val fb1 = FooBar(Some("1"), Foo(Some("A"), None), Some(Bar("A", 4)))
fb1: FooBar = FooBar(Some(1),Foo(Some(A),None),Some(Bar(A,4)))
scala> val fb2 = FooBar(None, Foo(Some("updated"), Some(2)), None)
fb2: FooBar = FooBar(None,Foo(Some(updated),Some(2)),None)
scala> fb1 |+| fb2
res0: FooBar = FooBar(Some(1),Foo(Some(Aupdated),Some(2)),Some(Bar(A,4)))
Note that this combines values inside of Some
, which isn't exactly what the question asks for, but is mentioned by the OP in a comment on my other answer. If you want the replacing behavior you can define the appropriate Monoid[Option[A]]
as in my other answer.
Using Kittens 1.0.0-M8, we're now able to derive a Semigroup
(I thought it was enough for this example, but Monoid
is a simply import away) without boilerplate at all:
import cats.implicits._
import cats.derived._, semigroup._, legacy._
case class Foo(fooPropA: Option[String], fooPropB: Option[Int])
case class Bar(barPropA: String, barPropB: Int)
case class FooBar(name: Option[String], foo: Foo, bar: Option[Bar])
val fb1 = FooBar(Some("1"), Foo(Some("A"), None), Some(Bar("A", 4)))
val fb2 = FooBar(None, Foo(Some("updated"), Some(2)), None)
println(fb1 |+| fb2)
Yields:
FooBar(Some(1),Foo(Some(Aupdated),Some(2)),Some(Bar(A,4)))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With