Merge two case classes in Scala, but with deeply nested types, without lens boilerplate

lenses, scala

Solution

One clean way to tackle this problem is to think of your merge operation as something like addition given the right set of monoid instances. You can see my answer here for a solution to a very similar problem, but the solution is even easier now thanks to the efforts of the typelevel team. First for the case classes:

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])

Then some boilerplate (which won't be necessary in the upcoming 2.0 release of Shapeless):

import shapeless._

implicit def fooIso = Iso.hlist(Foo.apply _, Foo.unapply _)
implicit def barIso = Iso.hlist(Bar.apply _, Bar.unapply _)
implicit def fooBarIso = Iso.hlist(FooBar.apply _, FooBar.unapply _)

I'm going to cheat just a little for the sake of clarity and put the "second" monoid instance for `Option` into scope instead of using tags:

import scalaz._, Scalaz._
import shapeless.contrib.scalaz._

implicit def optionSecondMonoid[A] = new Monoid[Option[A]] {
  val zero = None
  def append(a: Option[A], b: => Option[A]) = b orElse a
}

And we're done:

scala> val fb1 = FooBar(Some("1"), Foo(Some("A"), None), Some(Bar("A", 4)))
fb1: FooBar = FooBar(Some(one),Foo(Some(propA),None),Some(Bar(propA,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(updated),Some(2)),Some(Bar(A,4)))

See my previous answer for some additional discussion.

Problem

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?

Original source

Related problems