13

类似于这个案例类问题,但有一个转折:

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

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。

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

有没有更有效的方法来做到这一点?

4

3 回答 3

10

解决此问题的一种干净方法是将合并操作视为给定正确的 monoid 实例集的加法。您可以在此处查看我的答案,以获得非常相似的问题的解决方案,但是由于类型级别团队的努力,现在解决方案变得更加容易。首先是案例类:

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

然后是一些样板文件(在即将发布的 Shapeless 2.0 版本中不需要):

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放入范围而不是使用标签:

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

有关其他讨论,请参阅我之前的答案。

于 2013-08-02T21:49:45.067 回答
5

我之前的答案使用了 Shapeless 1.2.4、Scalaz 和 shapeless-contrib,而 Shapeless 1.2.4 和 shapeless-contrib 在这一点上已经过时了(两年后),所以这里是使用 Shapeless 2.2.5 和的更新答案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]]在我的其他答案中定义适当的行为。

于 2015-11-09T16:38:41.757 回答
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)))
于 2017-07-16T12:20:44.593 回答