Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

scala

lenses

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?

like image 754
mhamrah Avatar asked Aug 02 '13 21:08

mhamrah


2 Answers

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.

like image 189
Travis Brown Avatar answered Nov 16 '22 23:11

Travis Brown


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)))
like image 2
Yuval Itzchakov Avatar answered Nov 16 '22 23:11

Yuval Itzchakov