与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类。
有没有更有效的方法来做这件事?
答案 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)))