Frage

sagen, ich habe folgende zwei case classes bekommt:

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

und die folgende Instanz Person Klasse:

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

Wenn ich nun auf Update zipCode von raj will, dann muß ich tun:

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

Mit mehr Ebenen dies der Verschachtelung wird noch hässlicher. Gibt es eine saubere Art und Weise (so etwas wie Clojure ist update-in), wie verschachtelte Strukturen zu aktualisieren?

War es hilfreich?

Lösung

Reißverschluss

Huet Zipper bietet bequemes Traversal und 'Mutation' eine unveränderlichen Datenstruktur. Scalaz bietet Reißverschlüsse für Stream ( scalaz.Zipper ), und Tree ( scalaz.TreeLoc ). Es stellt sich heraus, daß die Struktur des Reißverschlusses aus der ursprünglichen Datenstruktur, in einer Art und Weise automatisch ableitbar ist, dass ähnelt symbolische Differenzierung eines algebraischen Ausdrucks.

Aber wie funktioniert diese Hilfe Sie mit Ihren Scala Fallklassen? Nun, Lukas Rytz kürzlich prototypische eine Erweiterung scalac, die automatisch Reißverschlüsse für kommentierten Fallklassen schaffen würde . Ich werde sein Beispiel reproduzieren hier:

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)

So dass die Gemeinschaft muss das Scala-Team davon zu überzeugen, dass diese Bemühungen fortgesetzt und in die Compilers werden sollten.

Im Übrigen Lukas kürzlich eine Version von Pacman, benutzerprogrammierbare über einen DSL veröffentlicht . Sieht nicht wie er den modifizierten Compiler verwendet, obwohl, wie ich keine @zip Anmerkungen sehen können.

Baum Rewriting

Unter anderen Umständen könnten Sie gerne einige Transformation über die gesamte Datenstruktur anzuwenden, nach einiger Strategie (top-down, Bottom-up) und basierte auf Regeln, die Partie gegen den Wert an einem gewissen Punkt in der Struktur. Das klassische Beispiel ist ein AST für eine Sprache umzuwandeln, vielleicht zu bewerten, zu vereinfachen, oder Informationen zu sammeln. Kiama unterstützt Umschreiben, die Beispiele in RewriterTests , und beobachten Sie diese Schritte außerhalb das Typsystem, dies zu erreichen.

Andere Tipps

Komisch, dass niemand hinzugefügt Linsen, da sie für diese Art von Sachen gemacht wurden. Also, hier ist ein CS Hintergrund Papier darauf, < a href = "http://debasishg.blogspot.com/2011/01/cqrs-with-akka-actors-and-functional.html" rel = "noreferrer"> hier ist ein Blog, die berühren kurz auf Linsen in Scala verwenden, hier ist ein Linsen-Implementierung für Scalaz und hier ist einige Code es, die überraschend wie Ihre Frage mit aussieht. Und auf Kesselblech zu senken, hier ein Plugin, das Scalaz Objektiv für Fallklassen erzeugen.

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

Die große Sache über Linsen ist, dass sie zusammensetzbare sind. So dass sie ein wenig umständlich auf den ersten, aber sie halten an Boden gewinnen desto mehr nutzen Sie sie. Außerdem sind sie ideal für Testbarkeit, da Sie nur einzelne Linsen müssen testen und können selbstverständlich ihre Zusammensetzung nehmen.

So, basierend auf einer Implementierung am Ende dieser Antwort zur Verfügung gestellt, hier ist, wie Sie es mit Linsen tun würden. Zuerst declare Linsen eine Postleitzahl in einer Adresse zu ändern, und eine Adresse in einer Person:

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

Nun, komponierten sie eine Linse zu bekommen, dass Änderungen in einer Person zipcode:

val personZipCodeLens = personAddressLens andThen addressZipCodeLens

Schließlich verwendet, dass die Linse zu ändern raj:

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

Oder mit etwas syntaktischen Zucker:

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

Oder auch:

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

Hier ist die einfache Implementierung von Scalaz genommen, für dieses Beispiel verwendet:

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
}

Nützliche Tools zum Einsatz Objektiven:

Ich will nur hinzufügen, dass die Macrocosm und Rillit Projekte, basierend auf Scala 2.10 Makros liefert dynamische Lens Creation.


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

Mit Macrocosm:

  

Dies funktioniert auch für Fallklassen definiert im aktuellen Kompilierungslaufes.

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

Ich habe für das, was Scala Bibliothek umsah, die die schönste Syntax hat und die beste Funktionalität und eine Bibliothek hier nicht erwähnt ist Monokel für mich, das wirklich gut war. Es folgt ein Beispiel:

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, um die Linsen zu kombinieren. Scalaz zum Beispiel erfordert viel vorformulierten und dieses compiles schnell und läuft sehr gut.

Um sie in Ihrem Projekt verwendet nur diese legt zum Abhängigkeit:

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
)

Shapeless funktioniert der 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)
    }
  }
}

, dass Hinweis, während einige andere Antworten hier können Sie Linsen komponieren eine gegebene Struktur gehen tiefer in diese shapless Linsen (und andere Bibliotheken / Makros) können Sie kombinieren zwei unabhängige Linsen, so dass Sie Objektiv können, dass Sätze eine beliebige Anzahl von Parametern in beliebige Positionen in Ihrer Struktur. Für komplexe Datenstrukturen, die zusätzliche Zusammensetzung ist sehr hilfreich.

Aufgrund ihrer zusammensetzbare Natur bieten Linsen eine sehr schöne Lösung für das Problem der stark verschachtelte Strukturen. Jedoch mit einem niedrigen Niveau der Verschachtelung, hat ich manchmal Linsen fühlt sich ein bisschen zu viel, und ich will nicht die ganzen Linsen einzuführen nähern, wenn es nur wenige Orte mit verschachteltem Updates ist. Der Vollständigkeit halber ist hier eine sehr einfache / pragmatische Lösung für diesen Fall:

Was ich tue, ist, einfach ein paar modify... Helferfunktionen in der obersten Ebene Struktur zu schreiben, die sich mit der hässlichen verschachtelten Kopie. 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 (das Update auf Client-Seite vereinfacht) erreicht wird:

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

den vollen Satz von modify Helfer zu schaffen, ist offensichtlich ärgerlich. Aber für interne Sachen ist es oft in Ordnung, nur um sie zum ersten Mal erstellen Sie versuchen, ein bestimmtes verschachteltes Feld zu ändern.

Vielleicht QuickLens entspricht Ihre Frage besser. QuickLens verwendet Makros einen IDE-freundlichen Ausdruck in etwas zu konvertieren, die nahe an die Originalkopie-Anweisung ist.

Da die beiden Beispielfälle Klassen:

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

und die Instanz von Person-Klasse:

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

Sie können zipCode von raj aktualisieren mit:

import com.softwaremill.quicklens._
val updatedRaj = raj.modify(_.address.zipCode).using(_ + 1)
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top