Czystszy sposób aktualizacji zagnieżdżonych struktur

Say I have got following two case classes:

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

I następująca instancja klasy Person:

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

Teraz jeśli chcę zaktualizować zipCode z raj to będę musiał zrobić:

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

Z większą ilością poziomów zagnieżdżania to staje się jeszcze brzydsze. Czy jest jakiś czystszy sposób (coś jak Clojure update-in), aby zaktualizować takie zagnieżdżone struktury?

Author: retronym, 2010-10-10

7 answers

Suwaki

Zamek błyskawiczny Huet zapewnia wygodne przemieszczanie i "mutację" niezmiennej struktury danych. Scalaz zapewnia zamki błyskawiczne do Stream (scalaz.Zipper ), oraz Tree (scalaz.TreeLoc ). Okazuje się, że struktura suwaka jest automatycznie wyprowadzana z pierwotnej struktury danych, w sposób przypominający symboliczne różnicowanie wyrażenia algebraicznego.

Ale jak to pomoże Ci z klasami przypadków Scali? Lukas Rytz ostatnio prototypował rozszerzenie do scalac, które automatycznie tworzyłoby suwaki dla klas z adnotacjami. Powtórzę jego przykład tutaj:

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)

Dlatego społeczność musi przekonać zespół Scali, że ten wysiłek powinien być kontynuowany i zintegrowany z kompilatorem.

Lukas opublikował niedawno wersję Pacmana, programowalną przez DSL. Nie wygląda na to, że używał zmodyfikowanego kompilatora, ponieważ nie widzę żadnego @zip Przypisy

Przepisywanie Drzewa

W innych okolicznościach można zastosować pewną transformację w całej strukturze danych, zgodnie z pewną strategią (odgórnie, oddolnie) i w oparciu o reguły, które w pewnym momencie są zgodne z wartością struktury. Klasycznym przykładem jest przekształcenie AST dla języka, być może w celu oceny, uproszczenia lub zebrania informacji. Kiama obsługuje przepisywanie , zobacz przykłady w RewriterTests, oraz zobacz to wideo . Oto fragment na zaostrzenie apetytu:

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

Zauważ, że Kiama wychodzi poza system typów, aby to osiągnąć.

 94
Author: retronym,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-10 23:57:35

Zabawne, że nikt nie dodawał soczewek, ponieważ zostały stworzone do tego typu rzeczy. Tak więc, tutaj jest tło CS na ten temat, tutaj {[8] } jest blog, który porusza krótko o używaniu soczewek w Scali, tutaj {[8] } jest implementacja soczewek dla Scalaz i tutaj Jest jakiś kod, który wygląda zaskakująco jak twoje pytanie. I, aby zmniejszyć płytkę kotła, Oto plugin, który generuje soczewki Scalaz dla klas case.

Za punkty bonusowe, Oto kolejne pytanie S. O., które dotyka soczewek, i papier autorstwa Tony ' ego Morrisa.

Wielką zaletą soczewek jest to, że są one komponowalne. Na początku są nieco uciążliwe, ale zyskują na popularności, im więcej ich używasz. Ponadto są one świetne do testowania, ponieważ wystarczy przetestować pojedyncze soczewki i można wziąć za pewnik ich skład.

Więc, na podstawie implementacji podanej na końcu tej odpowiedzi, oto jak to zrobić z soczewki. Po pierwsze zadeklarować zmianę kodu pocztowego w adresie, a adresu w osobie:

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

Teraz skomponuj je, aby uzyskać obiektyw, który zmienia kod zipcode w osobie:

val personZipCodeLens = personAddressLens andThen addressZipCodeLens

Wreszcie, użyj tego obiektywu, aby zmienić raj:

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

Lub, używając jakiegoś cukru składniowego:

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

Lub nawet:

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

Oto prosta implementacja, zaczerpnięta z Scalaz, użyta w tym przykładzie:

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
}
 179
Author: Daniel C. Sobral,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:02:12

Przydatne narzędzia do używania soczewek:

Chcę tylko dodać, że projekty makrokosmos i Rillit, oparte na makrach Scala 2.10, zapewniają dynamiczne tworzenie obiektywów.


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

Korzystanie Z Makrokosmosu:

Działa to nawet dla klas przypadków zdefiniowanych w bieżącym uruchomieniu kompilacji.

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
Author: Sebastien Lorber,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-12 04:38:01

Rozglądałem się za biblioteką Scali, która ma najładniejszą składnię i najlepszą funkcjonalność, a jedną biblioteką nie wymienioną tutaj jest monocle , która dla mnie była naprawdę dobra. Przykład:

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"))
Są bardzo ładne i istnieje wiele sposobów łączenia soczewek. Scalaz na przykład wymaga dużo boilerplate i to kompiluje szybko i działa świetnie.

Aby użyć ich w projekcie wystarczy dodać to do swoich zależności:

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
)
 8
Author: Johan S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-21 17:56:55

Shapeless robi sztuczkę:

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

Z:

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

Zauważ, że podczas gdy niektóre inne odpowiedzi pozwalają komponować soczewki, aby wejść głębiej w daną strukturę, te bezkształtne soczewki (i inne biblioteki/makra) pozwalają połączyć dwie niepowiązane soczewki, tak aby można było stworzyć soczewkę, która ustawia dowolną liczbę parametrów w dowolnych pozycjach w Twojej strukturze. W przypadku złożonych struktur danych bardzo pomocna jest dodatkowa kompozycja.

 7
Author: simbo1905,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-06 09:53:51

Ze względu na swój składowy charakter soczewki stanowią bardzo ładne rozwiązanie problemu silnie zagnieżdżonych struktur. Jednak przy niskim poziomie zagnieżdżania czasami czuję, że soczewki są trochę za duże i nie chcę wprowadzać całego podejścia do soczewek, jeśli jest tylko kilka miejsc z zagnieżdżonymi aktualizacjami. Ze względu na kompletność, oto bardzo proste / pragmatyczne rozwiązanie dla tego przypadku: {]}

To, co robię, to po prostu napisać kilka modify... pomocniczych funkcji w strukturze najwyższego poziomu, które zajmują z brzydką zagnieżdżoną kopią. Na przykład:

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

Mój główny cel (uproszczenie aktualizacji po stronie klienta) został osiągnięty:

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

Tworzenie pełnego zestawu pomocników modyfikacji jest oczywiście irytujące. Ale dla wewnętrznych rzeczy często jest w porządku, aby po prostu utworzyć je za pierwszym razem, gdy próbujesz zmodyfikować pewne zagnieżdżone pole.

 6
Author: bluenote10,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-19 09:18:02

Być może QuickLens lepiej pasuje do twojego pytania. QuickLens używa makr, aby przekonwertować przyjazne dla IDE wyrażenie na coś, co jest bliskie oryginalnej instrukcji copy.

Biorąc pod uwagę dwie przykładowe klasy przypadków:

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

I instancja klasy Person:

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

Możesz zaktualizować kod zipCode raj za pomocą:

import com.softwaremill.quicklens._
val updatedRaj = raj.modify(_.address.zipCode).using(_ + 1)
 3
Author: Erik van Oosten,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-20 08:09:41