web-dev-qa-db-de.com

Sauberer Weg, um verschachtelte Strukturen zu aktualisieren

Angenommen, ich habe folgende zwei case classes:

case class Address(street: String, city: String, state: String, zipCode: Int)
case class Person(firstName: String, lastName: String, address: Address)

und die folgende Instanz von Person class:

val raj = Person("Raj", "Shekhar", Address("M Gandhi Marg", 
                                           "Mumbai", 
                                           "Maharashtra", 
                                           411342))

Wenn ich nun zipCode von raj aktualisieren möchte, muss ich Folgendes tun:

val updatedRaj = raj.copy(address = raj.address.copy(zipCode = raj.address.zipCode + 1))

Mit mehr Verschachtelungsebenen wird dies noch hässlicher. Gibt es einen saubereren Weg (so etwas wie Clojures update-in) solche verschachtelten Strukturen aktualisieren?

121
missingfaktor

Reißverschlüsse

Huet's Zipper ermöglicht die bequeme Durchquerung und "Mutation" einer unveränderlichen Datenstruktur. Scalaz bietet Reißverschlüsse für Stream ( scalaz.Zipper ) und Tree ( scalaz.TreeLoc ). Es stellt sich heraus, dass die Struktur des Reißverschlusses automatisch von der ursprünglichen Datenstruktur abgeleitet werden kann, und zwar auf eine Weise, die der symbolischen Differenzierung eines algebraischen Ausdrucks ähnelt.

Aber wie hilft Ihnen das bei Ihren Scala case classes? Nun, Lukas Rytz kürzlich Prototyp einer Erweiterung von scalac, die automatisch Reißverschlüsse für mit Anmerkungen versehene case classes erstellt. I ' Ich werde sein Beispiel hier wiedergeben:

scala> @Zip case class Pacman(lives: Int = 3, superMode: Boolean = false) 
scala> @Zip case class Game(state: String = "pause", pacman: Pacman = Pacman()) 
scala> val g = Game() 
g: Game = Game("pause",Pacman(3,false))

// Changing the game state to "run" is simple using the copy method:
scala> val g1 = g.copy(state = "run") 
g1: Game = Game("run",Pacman(3,false))

// However, changing pacman's super mode is much more cumbersome (and it gets worse for deeper structures):
scala> val g2 = g1.copy(pacman = g1.pacman.copy(superMode = true))
g2: Game = Game("run",Pacman(3,true))

// Using the compiler-generated location classes this gets much easier: 
scala> val g3 = g1.loc.pacman.superMode set true
g3: Game = Game("run",Pacman(3,true)

Daher muss die Community das Scala) -Team davon überzeugen, dass diese Bemühungen fortgesetzt und in den Compiler integriert werden sollten.

Übrigens hat Lukas kürzlich veröffentlicht eine Version von Pacman, die vom Benutzer über DSL programmiert werden kann. Es sieht jedoch nicht so aus, als hätte er den modifizierten Compiler verwendet, da ich keine @Zip - Annotationen sehe.

Umschreiben von Bäumen

Unter anderen Umständen möchten Sie möglicherweise eine Transformation für die gesamte Datenstruktur anwenden, die einer Strategie (von oben nach unten, von unten nach oben) entspricht und auf Regeln basiert, die an einem bestimmten Punkt in der Struktur mit dem Wert übereinstimmen. Das klassische Beispiel ist die Transformation eines AST für eine Sprache, um möglicherweise Informationen zu bewerten, zu vereinfachen oder zu sammeln. Kiama unterstützt mschreiben , siehe Beispiele in RewriterTests , und sehen Sie sich dieses Video an. Hier ist ein Ausschnitt, um Ihren Appetit anzuregen:

// Test expression
val e = Mul (Num (1), Add (Sub (Var ("hello"), Num (2)), Var ("harold")))

// Increment every double
val incint = everywheretd (rule { case d : Double => d + 1 })
val r1 = Mul (Num (2), Add (Sub (Var ("hello"), Num (3)), Var ("harold")))
expect (r1) (rewrite (incint) (e))

Beachten Sie, dass Kiama Schritte außerhalb das Typensystem ist, um dies zu erreichen.

94
retronym

Komisch, dass niemand Linsen hinzufügte, da sie für diese Art von Sachen gemacht wurden. Also, hier ist ein CS-Hintergrundpapier, hier ist ein Blog, das kurz auf die Verwendung von Objektiven in Scala eingeht, hier ist eine Implementierung von Objektiven für Scalaz und hier wird ein Code verwendet, der Ihrer Frage überraschend ähnlich sieht. Und, um die Kesselplatte zu verkleinern, hier ein Plugin, das Scalaz-Objektive für Fallklassen generiert.

Für Bonuspunkte hier eine weitere S.O. Frage, die Linsen berührt, und ein Papier von Tony Morris.

Das große Problem bei Objektiven ist, dass sie zusammensetzbar sind. Sie sind anfangs etwas umständlich, gewinnen aber immer mehr an Boden, je mehr Sie sie verwenden. Sie eignen sich auch hervorragend für die Testbarkeit, da Sie nur einzelne Linsen testen müssen und deren Zusammensetzung als selbstverständlich voraussetzen können.

Anhand einer Implementierung, die am Ende dieser Antwort bereitgestellt wird, erfahren Sie, wie Sie dies mit Objektiven tun. Deklarieren Sie zunächst Linsen, um eine Postleitzahl in einer Adresse und eine Adresse in einer Person zu ändern:

val addressZipCodeLens = Lens(
    get = (_: Address).zipCode,
    set = (addr: Address, zipCode: Int) => addr.copy(zipCode = zipCode))

val personAddressLens = Lens(
    get = (_: Person).address, 
    set = (p: Person, addr: Address) => p.copy(address = addr))

Stellen Sie sie nun zusammen, um ein Objektiv zu erhalten, das die Postleitzahl einer Person ändert:

val personZipCodeLens = personAddressLens andThen addressZipCodeLens

Verwenden Sie zum Schluss dieses Objektiv, um Raj zu wechseln:

val updatedRaj = personZipCodeLens.set(raj, personZipCodeLens.get(raj) + 1)

Oder mit etwas syntaktischem Zucker:

val updatedRaj = personZipCodeLens.set(raj, personZipCodeLens(raj) + 1)

Oder auch:

val updatedRaj = personZipCodeLens.mod(raj, Zip => Zip + 1)

Hier ist die einfache Implementierung aus Scalaz, die für dieses Beispiel verwendet wurde:

case class Lens[A,B](get: A => B, set: (A,B) => A) extends Function1[A,B] with Immutable {
  def apply(whole: A): B   = get(whole)
  def updated(whole: A, part: B): A = set(whole, part) // like on immutable maps
  def mod(a: A, f: B => B) = set(a, f(this(a)))
  def compose[C](that: Lens[C,A]) = Lens[C,B](
    c => this(that(c)),
    (c, b) => that.mod(c, set(_, b))
  )
  def andThen[C](that: Lens[B,C]) = that compose this
}
182

Nützliche Tools zum Verwenden von Objektiven:

Ich möchte nur hinzufügen, dass die Projekte Macrocosm und Rillit , die auf den Makros Scala= 2.10) basieren, die dynamische Linsenerstellung ermöglichen.


Verwenden von Rillit:

case class Email(user: String, domain: String)
case class Contact(email: Email, web: String)
case class Person(name: String, contact: Contact)

val person = Person(
  name = "Aki Saarinen",
  contact = Contact(
    email = Email("aki", "akisaarinen.fi"),
    web   = "http://akisaarinen.fi"
  )
)

scala> Lenser[Person].contact.email.user.set(person, "john")
res1: Person = Person(Aki Saarinen,Contact(Email(john,akisaarinen.fi),http://akisaarinen.fi))

Verwenden von Macrocosm:

Dies funktioniert sogar für Fallklassen, die im aktuellen Kompilierungslauf definiert wurden.

case class Person(name: String, age: Int)

val p = Person("brett", 21)

scala> lens[Person].name._1(p)
res1: String = brett

scala> lens[Person].name._2(p, "bill")
res2: Person = Person(bill,21)

scala> lens[Person].namexx(()) // Compilation error
11

Ich habe mich nach der Scala Bibliothek umgesehen, die die schönste Syntax und die beste Funktionalität aufweist, und eine Bibliothek, die hier nicht erwähnt wird, ist Monokel , was für mich wirklich gut war. Ein Beispiel folgt:

import monocle.Macro._
import monocle.syntax._

case class A(s: String)
case class B(a: A)

val aLens = mkLens[B, A]("a")
val sLens = aLens |-> mkLens[A, String]("s")

//Usage
val b = B(A("hi"))
val newB = b |-> sLens set("goodbye") // gives B(A("goodbye"))

Diese sind sehr schön und es gibt viele Möglichkeiten, die Linsen zu kombinieren. Scalaz zum Beispiel verlangt viel Boilerplate und das lässt sich schnell kompilieren und läuft großartig.

Um sie in Ihrem Projekt zu verwenden, fügen Sie dies einfach zu Ihren Abhängigkeiten hinzu:

resolvers ++= Seq(
  "Sonatype OSS Releases"  at "http://oss.sonatype.org/content/repositories/releases/",
  "Sonatype OSS Snapshots" at "http://oss.sonatype.org/content/repositories/snapshots/"
)

val scalaVersion   = "2.11.0" // or "2.10.4"
val libraryVersion = "0.4.0"  // or "0.5-SNAPSHOT"

libraryDependencies ++= Seq(
  "com.github.julien-truffaut"  %%  "monocle-core"    % libraryVersion,
  "com.github.julien-truffaut"  %%  "monocle-generic" % libraryVersion,
  "com.github.julien-truffaut"  %%  "monocle-macro"   % libraryVersion,       // since 0.4.0
  "com.github.julien-truffaut"  %%  "monocle-law"     % libraryVersion % test // since 0.4.0
)
9
Johan S

Aufgrund ihrer zusammensetzbaren Natur bieten Linsen eine sehr gute Lösung für das Problem stark verschachtelter Strukturen. Bei einem geringen Verschachtelungsgrad empfinde ich Objektive manchmal als etwas zu viel, und ich möchte nicht den gesamten Objektivansatz einführen, wenn es nur wenige Stellen mit verschachtelten Aktualisierungen gibt. Der Vollständigkeit halber ist hier eine sehr einfache/pragmatische Lösung für diesen Fall:

Ich schreibe einfach ein paar modify... Hilfsfunktionen in der Struktur der obersten Ebene, die sich mit der hässlichen verschachtelten Kopie befassen. Zum Beispiel:

case class Person(firstName: String, lastName: String, address: Address) {
  def modifyZipCode(modifier: Int => Int) = 
    this.copy(address = address.copy(zipCode = modifier(address.zipCode)))
}

Mein Hauptziel (Vereinfachung des Updates auf Client-Seite) ist erreicht:

val updatedRaj = raj.modifyZipCode(_ => 41).modifyZipCode(_ + 1)

Das Erstellen des vollständigen Satzes von Änderungshilfen ist offensichtlich ärgerlich. Aber für interne Aufgaben ist es oft in Ordnung, sie nur zu erstellen, wenn Sie zum ersten Mal versuchen, ein bestimmtes verschachteltes Feld zu ändern.

7
bluenote10

Formlos macht den Trick:

"com.chuusai" % "shapeless_2.11" % "2.0.0"

mit:

case class Address(street: String, city: String, state: String, zipCode: Int)
case class Person(firstName: String, lastName: String, address: Address)

object LensSpec {
      import shapeless._
      val zipLens = lens[Person] >> 'address >> 'zipCode  
      val surnameLens = lens[Person] >> 'firstName
      val surnameZipLens = surnameLens ~ zipLens
}

class LensSpec extends WordSpecLike with Matchers {
  import LensSpec._
  "Shapless Lens" should {
    "do the trick" in {

      // given some values to recreate
      val raj = Person("Raj", "Shekhar", Address("M Gandhi Marg",
        "Mumbai",
        "Maharashtra",
        411342))
      val updatedRaj = raj.copy(address = raj.address.copy(zipCode = raj.address.zipCode + 1))

      // when we use a lens
      val lensUpdatedRaj = zipLens.set(raj)(raj.address.zipCode + 1)

      // then it matches the explicit copy
      assert(lensUpdatedRaj == updatedRaj)
    }

    "better yet chain them together as a template of values to set" in {

      // given some values to recreate
      val raj = Person("Raj", "Shekhar", Address("M Gandhi Marg",
        "Mumbai",
        "Maharashtra",
        411342))

      val updatedRaj = raj.copy(firstName="Rajendra", address = raj.address.copy(zipCode = raj.address.zipCode + 1))

      // when we use a compound lens
      val lensUpdatedRaj = surnameZipLens.set(raj)("Rajendra", raj.address.zipCode+1)

      // then it matches the explicit copy
      assert(lensUpdatedRaj == updatedRaj)
    }
  }
}

Beachten Sie, dass Sie bei einigen anderen Antworten Linsen zusammenstellen können, um eine bestimmte Struktur zu untersuchen. Mit diesen formlosen Linsen (und anderen Bibliotheken/Makros) können Sie zwei unabhängige Linsen kombinieren, sodass Sie Linsen erstellen können, die eine beliebige Anzahl von Parametern in beliebige Positionen setzen in deiner Struktur. Bei komplexen Datenstrukturen ist diese zusätzliche Komposition sehr hilfreich.

7
simbo1905

Vielleicht passt QuickLens besser zu Ihrer Frage. QuickLens verwendet Makros, um einen freundlichen Ausdruck IDE) in einen Ausdruck zu konvertieren, der der ursprünglichen Kopieranweisung nahe kommt.

Angesichts der beiden Beispielfallklassen:

case class Address(street: String, city: String, state: String, zipCode: Int)
case class Person(firstName: String, lastName: String, address: Address)

und die Instanz der Personenklasse:

val raj = Person("Raj", "Shekhar", Address("M Gandhi Marg", 
                                           "Mumbai", 
                                           "Maharashtra", 
                                           411342))

sie können den zipCode von raj aktualisieren mit:

import com.softwaremill.quicklens._
val updatedRaj = raj.modify(_.address.zipCode).using(_ + 1)
4
Erik van Oosten