在Scala中合并两个case类,但具有深度嵌套类型,而无需使用镜头样板代码。

13

类似于这个case class问题,但有一个小变化:

我有一个case class作为属性包含了一些深度嵌套的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, 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方法返回元组,迭代并将元组组合成新的元组,然后将元组应用回案例类。

有没有更有效的方法来处理这个问题?

3个回答

10

处理这个问题的一个简明方法是将您的合并操作视为加法,给定正确的幺半群实例集合。您可以查看此处我对非常类似问题的解决方案,但由于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])

接下来是一些样板代码(在即将发布的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)))

请参见我的上一个答案以获取更多讨论。

Travis,感謝你的解決方案。這是一種非常有趣的方法。但有一個問題:如果我們要遞歸組合兩個可能具有Option子屬性的Some(_)值該怎麼辦?例如,如果FooBar中的foo屬性是foo:Option [Foo],並且我想將|+|應用於fooPropA和fooPropB中,該怎麼辦? - mhamrah
1
这正是Option的默认monoid实例的行为,第二个实例在这里被覆盖(这是您想要的Option [String]Option [Int]字段)。您可以使用tags混合和匹配这些行为-如果您有兴趣,我稍后可以写一个快速示例。 - Travis Brown
谢谢Travis,我真的很感激你提供一个示例。我对Scalaz和Shapeless还不熟悉-我知道我想要发生什么,但不确定如何实现。我认为当代码到达可以转换为Hlist的元素时,将其转换并将|+|应用于内部hlist,然后再转换回案例类。我也会更新我的问题,包括这种情况。 - mhamrah
你是否有 Shapeless 2.x 的答案呢? - kali
@kali 完成(在新答案中)。 - Travis Brown

5

我的先前回答使用了Shapeless 1.2.4,Scalaz和shapeless-contrib。而Shapeless 1.2.4和shapeless-contrib已经过时了(两年多之后),所以这里提供一个更新的答案,使用Shapeless 2.2.5和cats0.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项目(或类似的项目)可能会为cats的类型类提供这种功能(类似于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]]

我尝试了一下,但没有成功。我有一种隐式缺失的印象?我添加了一个implicitly [Monoid [Foo]],但得到了与https://dev59.com/5V8e5IYBdhLWcg3wpLsr#25559471中解释的相同错误,但我无法将那里的解决方案适应到这里。 - Davi
好的,我刚刚尝试了一下使用Scala 2.11而不是Scala 2.10,然后它完美地运行了。非常感谢! - Davi
@Davi,是的,抱歉——在2.10上,您需要使用Macro Paradise编译器插件才能使通用派生工作。 - Travis Brown

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接