web-dev-qa-db-fra.com

Une façon plus propre de mettre à jour les structures imbriquées

Dis que j'ai suivi deux case classes:

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

et l'instance suivante de la classe Person:

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

Maintenant, si je veux mettre à jour zipCode de raj alors je devrai faire:

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

Avec plus de niveaux d'imbrication, cela devient encore plus laid. Y a-t-il un moyen plus propre (quelque chose comme le Clojure's update-in) pour mettre à jour ces structures imbriquées?

121
missingfaktor

Fermetures éclair

Zipper de Huet fournit une traversée et une "mutation" pratiques d'une structure de données immuable. Scalaz fournit des fermetures à glissière pour Stream ( scalaz.Zipper ) et Tree ( scalaz.TreeLoc ). Il s'avère que la structure de la fermeture éclair est automatiquement dérivable de la structure de données d'origine, d'une manière qui ressemble à la différenciation symbolique d'une expression algébrique.

Mais comment cela vous aide-t-il avec vos classes de cas Scala? Eh bien, Lukas Rytz a récemment prototypé une extension de scalac qui créerait automatiquement des fermetures à glissière pour les classes de cas annotées. I ' Je reproduis ici son exemple:

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)

La communauté doit donc convaincre l'équipe Scala que cet effort doit être poursuivi et intégré dans le compilateur.

Soit dit en passant, Lukas a récemment publié une version de Pacman, programmable par l'utilisateur via une DSL. Il ne semble pas avoir utilisé le compilateur modifié, car je ne vois aucun @Zip annotations.

Réécriture d'arbre

Dans d'autres circonstances, vous souhaiterez peut-être appliquer une transformation à l'ensemble de la structure de données, selon une stratégie (descendante, ascendante) et basée sur des règles qui correspondent à la valeur à un moment donné de la structure. L'exemple classique consiste à transformer un AST pour une langue, peut-être pour évaluer, simplifier ou collecter des informations. Kiama prend en charge Réécriture , voir le exemples dans RewriterTests , et regardez ceci vidéo . Voici un extrait pour vous mettre en appétit:

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

Notez que Kiama étapes à l'extérieur le système de type pour y parvenir.

94
retronym

C'est drôle que personne n'ait ajouté de lentilles, car elles étaient faites pour ce genre de choses. Donc, ici est un document d'information CS à ce sujet, ici est un blog qui aborde brièvement l'utilisation des lentilles dans Scala, ici est une implémentation des lentilles pour Scalaz et ici est un code qui l'utilise, qui ressemble étonnamment à votre question. Et, pour réduire la plaque de la chaudière, voici un plugin qui génère des lentilles Scalaz pour les classes de cas.

Pour les points bonus, voici un autre S.O. question qui touche aux lentilles, et un papier par Tony Morris.

Le gros problème avec les lentilles, c'est qu'elles sont composables. Ils sont donc un peu encombrants au début, mais ils gagnent du terrain au fur et à mesure que vous les utilisez. En outre, ils sont parfaits pour la testabilité, car vous n'avez qu'à tester des lentilles individuelles et pouvez tenir pour acquis leur composition.

Donc, sur la base d'une implémentation fournie à la fin de cette réponse, voici comment vous le feriez avec des lentilles. Tout d'abord, déclarez les lentilles pour changer un code postal dans une adresse et une adresse chez une personne:

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

Maintenant, composez-les pour obtenir un objectif qui change le code postal chez une personne:

val personZipCodeLens = personAddressLens andThen addressZipCodeLens

Enfin, utilisez cet objectif pour changer raj:

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

Ou, en utilisant du sucre syntaxique:

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

Ou même:

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

Voici l'implémentation simple, tirée de Scalaz, utilisée pour cet exemple:

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
Daniel C. Sobral

Outils utiles pour utiliser les objectifs:

Je veux juste ajouter que les projets Macrocosm et Rillit , basés sur Scala 2.10 macros, fournissent la création dynamique d'objectifs).


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

Utilisation de macrocosme:

Cela fonctionne même pour les classes de cas définies dans la compilation en cours.

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

J'ai cherché ce qui Scala bibliothèque qui a la plus belle syntaxe et la meilleure fonctionnalité et une bibliothèque non mentionnée ici est monocle qui pour moi a été vraiment bonne Voici un exemple:

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

Ce sont très gentils et il existe de nombreuses façons de combiner les lentilles. Scalaz, par exemple, demande beaucoup de passe-partout et cela se compile rapidement et fonctionne très bien.

Pour les utiliser dans votre projet, ajoutez simplement ceci à vos dépendances:

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

En raison de leur nature composable, les lentilles offrent une très belle solution au problème des structures fortement imbriquées. Cependant, avec un faible niveau d'imbrication, j'ai parfois l'impression que les lentilles sont un peu trop, et je ne veux pas introduire l'approche des lentilles s'il n'y a que peu d'endroits avec des mises à jour imbriquées. Par souci d'exhaustivité, voici une solution très simple/pragmatique pour ce cas:

Ce que je fais est d'écrire simplement quelques modify... fonctions d'assistance dans la structure de niveau supérieur, qui traitent de la copie imbriquée laide. Par exemple:

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

Mon objectif principal (simplifier la mise à jour côté client) est atteint:

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

La création de l'ensemble complet des assistants de modification est évidemment ennuyeuse. Mais pour les choses internes, il est souvent correct de les créer la première fois que vous essayez de modifier un certain champ imbriqué.

7
bluenote10

Shapeless fait l'affaire:

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

avec:

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

Notez que tandis que d'autres réponses ici vous permettent de composer des objectifs pour aller plus loin dans une structure donnée, ces objectifs sans shapless (et d'autres bibliothèques/macros) vous permettent de combiner deux objectifs non liés de sorte que vous pouvez créer un objectif qui définit un nombre arbitraire de paramètres dans des positions arbitraires dans votre structure. Pour les structures de données complexes, cette composition supplémentaire est très utile.

7
simbo1905

Peut-être que QuickLens correspond mieux à votre question. QuickLens utilise des macros pour convertir une expression conviviale IDE en quelque chose qui est proche de l'instruction de copie d'origine.

Étant donné les deux exemples de classes de cas:

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

et l'instance de la classe Person:

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

vous pouvez mettre à jour zipCode de raj avec:

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