13

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?

Community
  • 1
  • 1
mhamrah
  • 9,038
  • 4
  • 24
  • 22

3 Answers3

10

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.

Community
  • 1
  • 1
Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • Travis, thanks for this solution. It's a very interesting approach. One question though: how can we recurse to combine two Some(_) values which may have Option sub properties? For instance, what if the foo property in FooBar was foo:Option[Foo], and I wanted to apply |+| to both fooPropA and fooPropB? – mhamrah Aug 05 '13 at 20:00
  • 1
    That's exactly the behavior of the default monoid instance for `Option`, which is overriden here by the second instance (which is what you want for the `Option[String]` and `Option[Int]` fields). You can mix and match the behaviors using [tags](https://github.com/scalaz/scalaz/blob/scalaz-seven/core/src/main/scala/scalaz/Tags.scala)—I could write up a quick example later if you're interested. – Travis Brown Aug 05 '13 at 20:10
  • Thanks Travis, I'd really appreciate a sample. I'm new to Scalaz and Shapeless- I think I know what I want to happen, but not sure how to implement. The way I see it is that when the code gets to an element in the Hlist that it can convert to an Hlist, convert it, and apply the |+| to the inner hlist and convert back to a case class. I'll update my question with this scenario as well. – mhamrah Aug 05 '13 at 21:32
  • Do you by any chance have a Shapeless 2.x answer for this now? – kali Nov 09 '15 at 16:07
  • @kali Done (in a new answer). – Travis Brown Nov 09 '15 at 16:39
5

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.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • I gave this a try, but had no success. I have the impression that some kind of implicit is missing? I added a implicitly[Monoid[Foo]], but got the same errors as explained in http://stackoverflow.com/questions/25517069/what-is-the-purpose-of-the-emptycoproduct-and-coproduct-methods-of-the-typeclass/25559471#25559471, but I wasn't able to adapt the solution from there to here. – Davi Nov 29 '15 at 14:16
  • Ok, just tried it again with scala 2.11 instead of scala 2.10 and then it works flawlessly. Thanks a lot! – Davi Nov 30 '15 at 17:31
  • @Davi, yes, sorry—on 2.10 you'd need the Macro Paradise compiler plugin to make the generic derivation work. – Travis Brown Nov 30 '15 at 17:43
2

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)))
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321