13
votes

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?

3

3 Answers

10
votes

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.

5
votes

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.

2
votes

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