在Scala中合并两个案例类,但是具有深层嵌套类型,没有镜头样板

时间:2013-08-02 21:11:10

标签: scala lenses

this case class question类似,但有一个转折:

我有一个case类,它有一些深层嵌套的case类作为属性。举个简单的例子,

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

我想将两个FooBar案例类合并在一起,获取输入存在的值并将它们应用于现有实例,从而生成更新版本:

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

我知道我可以使用镜头来构建深层嵌套的属性更新;但是,我觉得这需要大量的锅炉板代码:我需要一个镜头用于每个属性,而另一个组合镜头在父类中。即使在shapeless中使用更简洁的镜头创建方法,这似乎也需要维护很多。

棘手的部分是optionFoo元素:在这种情况下,两个元素都存在Some(value)。但是,我想合并内部选项属性,而不仅仅是用fb2的新值覆盖fb1。

我想知道是否有一种很好的方法可以将这两个值合并在一起,只需要最少的代码。我的直觉告诉我尝试在case类上使用unapply方法返回一个元组,迭代并将元组组合成一个新的元组,然后将元组应用回case类。

有没有更有效的方法来做这件事?

3 个答案:

答案 0 :(得分:10)

解决这个问题的一个简洁方法是将合并操作视为添加给定正确的monoid实例集合。您可以看到我的答案here来解决一个非常类似的问题,但由于the efforts团队的typelevel,解决方案现在变得更加容易了。首先是案例类:

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

然后是一些样板(在即将发布的2.0版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 _)

为了清晰起见,我将稍微作弊,并将Option的“第二个”monoid实例放入范围而不是使用标记:

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
}

我们已经完成了:

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

有关其他讨论,请参阅my previous answer

答案 1 :(得分:5)

我以前的回答使用了Shapeless 1.2.4,Scalaz和shapeless-contrib,而Shapeless 1.2.4和shapeless-contrib在这一点上已经过时了(两年后),所以这里是使用Shapeless 2.2的更新答案。 5和cats 0.3.0。我将假设这样的构建配置:

scalaVersion := "2.11.7"

libraryDependencies ++= Seq(
  "com.chuusai" %% "shapeless" % "2.2.5",
  "org.spire-math" %% "cats" % "0.3.0"
)

Shapeless现在包含一个我们可以在这里使用的ProductTypeClass类型类。最终Miles Sabin的kittens项目(或类似的东西)很可能为猫的类型类提供这种东西(类似于shapeless-contrib为Scalaz所扮演的角色),但现在只使用ProductTypeClass也不错:

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

然后:

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

最后:

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

请注意,这会合并Some内部的值,这不是问题所要求的,但OP在我的其他答案的评论中提到了。如果您想要替换行为,可以在我的其他答案中定义相应的Monoid[Option[A]]

答案 2 :(得分:2)

使用Kittens 1.0.0-M8,我们现在能够派生出一个Semigroup(我认为这个示例已足够了,但Monoid只是简单地导入了)而没有样板所有:

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)

收率:

FooBar(Some(1),Foo(Some(Aupdated),Some(2)),Some(Bar(A,4)))