2013-08-02 8 views
13

ähnlich this case class question aber mit einem Twist:Zusammenführen von zwei Fallklassen in Scala, aber mit tief verschachtelten Typen, ohne Objektiv vorformulierten

ich einen Fall Klasse habe, die einige tief verschachtelte Fall-Klassen als Eigenschaften aufweist. Als einfaches Beispiel,

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

Ich mag zwei Klassen FooBar Fall zusammen fusionieren, um die Werte nehmen, die für eine Eingabe existieren und sie zu einer vorhandenen Instanz anwenden, um eine aktualisierte Version produziert:

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

Ich weiß, dass ich ein Objektiv verwenden kann, um die tief verschachtelten Eigenschaftsaktualisierungen zu verfassen; Ich denke jedoch, dass dies eine Menge Kesselblech-Code erfordern wird: Ich brauche eine Linse für jede Eigenschaft und eine andere zusammengesetzte Linse in der Elternklasse. Dies scheint eine Menge zu halten, selbst wenn man den Ansatz der engeren Linsenerzeugung in shapeless verwendet.

Der schwierige Teil ist das ElementFoo Element: in diesem Szenario existieren beide Elemente mit einem Some (Wert). Allerdings möchte ich die inneren Option Eigenschaften zusammenführen, nicht nur fb1 mit fb2 neue Werte überschreiben.

Ich frage mich, ob es einen guten Ansatz gibt, diese beiden Werte auf eine Weise zusammenzuführen, die minimalen Code erfordert. Mein Bauchgefühl sagt mir, dass ich versuchen soll, die unapply-Methode für die Fallklasse zu verwenden, um ein Tupel zurückzugeben, zu iterieren und die Tupel zu einem neuen Tupel zu kombinieren und das Tupel dann auf eine Fallklasse anzuwenden.

Gibt es einen effizienteren Weg, dies zu tun?

Antwort

10

Eine saubere Möglichkeit, dieses Problem anzugehen, besteht darin, sich Ihre Merge-Operation als etwas Ähnliches vorzustellen, wenn Sie die richtigen Monoid-Instanzen verwenden. Sie können meine Antwort here für eine Lösung für ein sehr ähnliches Problem sehen, aber die Lösung ist jetzt noch einfacher dank the efforts der typelevel Team. Zunächst für den Fall Klassen:

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

Dann einige Textvorschlag (die in der kommenden Version 2.0 von Shapeless wird nicht nötig sein):

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

ich für die nur ein wenig betrügen werde und aus Gründen der Klarheit stellen die „zweite“ Monoid-Instanz für Option in Umfang statt mit Tags:

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 
} 

und wir sind fertig:

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

Weitere Erläuterungen finden Sie unter my previous answer.

+0

Travis, danke für diese Lösung. Es ist ein sehr interessanter Ansatz. Eine Frage: Wie können wir zwei Some (_) -Werte kombinieren, die Option Sub-Eigenschaften haben können? Zum Beispiel, was wäre, wenn die foo -Eigenschaft in FooBar foo wäre: Option [Foo], und ich wollte | + | anwenden zu beiden fooPropA und fooPropB? – mhamrah

+1

Das ist genau das Verhalten der Standard-Monoid-Instanz für "Option", das hier von der zweiten Instanz überschrieben wird (was Sie für die Felder "Option [String]" und "Option [Int]" wünschen). Sie können die Verhaltensweisen mithilfe von [tags] (https://github.com/scalaz/scalaz/blob/scalaz-seven/core/src/main/scala/scalaz/Tags.scala) mischen und anpassen - ich könnte eine schreiben schnelles Beispiel später, wenn Sie interessiert sind. –

+0

Danke Travis, ich würde wirklich eine Probe zu schätzen wissen. Ich bin neu bei Scalaz und Shapeless - ich glaube, ich weiß, was ich will, aber nicht sicher, wie ich es umsetzen soll. Die Art, wie ich es sehe ist, dass, wenn der Code zu einem Element in der Hlist kommt, das in eine Hlist konvertiert werden kann, konvertiert man es und wendet das | + | an in die innere Liste und konvertieren zurück in eine Fallklasse. Ich werde meine Frage auch mit diesem Szenario aktualisieren. – mhamrah

5

Meine vorherige Antwort verwendet Shapeless 1.2.4, Scalaz und Shapeless-Contrib, und Shapeless 1.2.4 und Shapeless-Contrib sind zu diesem Zeitpunkt ziemlich veraltet (über zwei Jahre später), also hier ist eine aktualisierte Antwort mit Shapeless 2.2 .5 und cats 0.3.0. Ich werde eine Build-Konfiguration wie folgt annehmen:

scalaVersion := "2.11.7" 

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

Shapeless enthält nun eine ProductTypeClass Typklasse, die wir hier verwenden können.Schließlich wird Miles Sabins kittens Projekt (oder etwas Ähnliches) wahrscheinlich solche Dinge für Katzen-Typklassen bereitstellen (ähnlich der Rolle, die für Scalaz Shapeless-Contrib gespielt hat), aber im Moment ist nur die Verwendung von ProductTypeClass nicht schlecht:

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

Und dann:

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

Und schließlich:

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

Beachten Sie, dass diese Werte kombiniert innerhalb von Some, was nicht genau das ist, was die Frage verlangt, aber vom OP in einem Kommentar zu meiner anderen Antwort erwähnt wird. Wenn Sie das ersetzende Verhalten wollen, können Sie das entsprechende Monoid[Option[A]] wie in meiner anderen Antwort definieren.

+0

Ich habe dies versucht, hatte aber keinen Erfolg. Ich habe den Eindruck, dass etwas implizites fehlt? Ich fügte implizit [Monoid [Foo]] hinzu, bekam aber die gleichen Fehler wie in http://stackoverflow.com/questions/25517069/what-is-the-purpose-of-the-emptycoproduct-and-coproduct-methods erklärt -of-typeclass/25559471 # 25559471, aber ich war nicht in der Lage, die Lösung von dort zu hier anzupassen. – Davi

+0

Ok, hab es mal wieder mit Scala 2.11 statt Scala 2.10 probiert und dann funktioniert es einwandfrei. Danke vielmals! – Davi

+0

@Davi, ja, sorry-on 2.10 Sie benötigen das Macro Paradise Compiler-Plugin, um die generische Ableitung durchzuführen. –

2

Kittens 1.0.0-M8 benutzen, sind wir nun in der Lage ein Semigroup abzuleiten (ich dachte, es für dieses Beispiel genug war, aber Monoid ist ein einfach importieren weg) überhaupt ohne vorformulierten:

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) 

Ausbeuten:

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