Manera más limpia de actualizar estructuras anidadas

Digamos que he seguido dos case class :

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

y la siguiente instancia de clase Person :

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

Ahora si quiero actualizar zipCode of raj , tendré que hacer lo siguiente:

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

Con más niveles de anidación, esto se vuelve aún más feo. ¿Hay alguna manera más limpia (algo así como la update-in de Clojure) para actualizar tales estructuras anidadas?

Cremalleras

Huet’s Zipper proporciona un recorrido conveniente y ‘mutación’ de una estructura de datos inmutable. Scalaz proporciona Zippers para Stream ( scalaz.Zipper ) y Tree ( scalaz.TreeLoc ). Resulta que la estructura de la cremallera se deriva automáticamente de la estructura de datos original, de una manera que se asemeja a la diferenciación simbólica de una expresión algebraica.

Pero, ¿cómo te ayuda esto con tus clases de casos de Scala? Bueno, Lukas Rytz recientemente creó un prototipo de una extensión para Scalac que crearía automáticamente cremalleras para las clases de caso anotadas. Reproduciré su ejemplo aquí:

 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) 

Entonces la comunidad necesita persuadir al equipo de Scala de que este esfuerzo debe continuar e integrarse en el comstackdor.

Por cierto, Lukas publicó recientemente una versión de Pacman, progtwigble por el usuario a través de una DSL. No parece que haya usado el comstackdor modificado, ya que no puedo ver las anotaciones @zip .

Reescritura de árbol

En otras circunstancias, le gustaría aplicar alguna transformación en toda la estructura de datos, de acuerdo con alguna estrategia (de arriba hacia abajo y de abajo hacia arriba) y basada en reglas que coincidan con el valor en algún punto de la estructura. El ejemplo clásico es transformar un AST para un idioma, tal vez para evaluar, simplificar o recostackr información. Kiama admite Reescribir , vea los ejemplos en RewriterTests y vea este video . Aquí hay un fragmento para abrir el apetito:

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

Tenga en cuenta que Kiama sale del sistema de tipos para lograr esto.

Es gracioso que nadie agregó lentes, ya que fueron HECHOS para este tipo de cosas. Entonces, aquí hay un documento de referencia de CS, aquí hay un blog que toca brevemente el uso de lentes en Scala, aquí hay una implementación de lentes para Scalaz y aquí hay algún código que lo usa, que sorprendentemente se parece a tu pregunta. Y, para reducir la placa de la caldera, aquí hay un complemento que genera lentes Scalaz para las clases de casos.

Para obtener puntos de bonificación, aquí hay otra pregunta de SO que toca lentes, y un documento de Tony Morris.

El gran problema con los lentes es que son composables. Así que son un poco engorrosos al principio, pero siguen ganando terreno cuanto más los utilizas. Además, son excelentes para la capacidad de prueba, ya que solo necesita probar lentes individuales, y puede dar por sentada su composición.

Por lo tanto, de acuerdo con una implementación que se brinda al final de esta respuesta, así es como lo harías con los lentes. Primero, declare las lentes para cambiar un código postal en una dirección y una dirección en una persona:

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

Ahora, compórtalas para obtener una lente que cambie el código postal en una persona:

 val personZipCodeLens = personAddressLens andThen addressZipCodeLens 

Finalmente, usa esa lente para cambiar raj:

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

O, usando un poco de azúcar sintáctico:

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

O incluso:

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

Aquí está la implementación simple, tomada de Scalaz, utilizada para este ejemplo:

 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 } 

Herramientas útiles para usar lentes:

Solo quiero agregar que los proyectos Macrocosm y Rillit , basados ​​en macros Scala 2.10, proporcionan creación dinámica de lentes.


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

Usando Macrocosmo:

Esto incluso funciona para clases de casos definidas en la ejecución de comstackción actual.

 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(()) // Comstacktion error 

He estado buscando la biblioteca Scala que tenga la mejor syntax y la mejor funcionalidad, y una biblioteca que no se menciona aquí es el monóculo, que para mí ha sido realmente bueno. Un ejemplo a continuación:

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

Estos son muy buenos y hay muchas formas de combinar las lentes. Scalaz, por ejemplo, exige una gran cantidad de repeticiones y esta comstack rápidamente y funciona muy bien.

Para usarlos en su proyecto, simplemente agregue esto a sus dependencias:

 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 hace el truco:

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

con:

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

Tenga en cuenta que, si bien algunas otras respuestas aquí le permiten componer lentes para profundizar en una estructura determinada, estas lentes Shapless (y otras bibliotecas / macros) le permiten combinar dos lentes no relacionadas para que pueda crear lentes que establezcan un número arbitrario de parámetros en posiciones arbitrarias en tu estructura Para estructuras de datos complejas, la composición adicional es muy útil.

Debido a su naturaleza composable, las lentes proporcionan una solución muy agradable al problema de las estructuras fuertemente anidadas. Sin embargo, con un nivel bajo de anidamiento, a veces siento que las lentes son demasiado, y no quiero introducir el enfoque de lentes enteras si solo hay pocos lugares con actualizaciones anidadas. En aras de la integridad, aquí hay una solución muy simple / pragmática para este caso:

Lo que hago es simplemente escribir algunas funciones de modify... ayuda en la estructura de nivel superior, que se ocupan de la copia anidada fea. Por ejemplo:

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

Mi objective principal (simplificar la actualización en el lado del cliente) se logra:

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

Crear el conjunto completo de ayudantes de modificación es obviamente molesto. Pero para cosas internas a menudo es correcto crearlas la primera vez que intenta modificar un cierto campo nested.

Tal vez QuickLens coincida con su pregunta mejor. QuickLens usa macro’s para convertir una expresión amigable IDE en algo que está cerca de la statement de copia original.

Dadas las dos clases de casos de ejemplo:

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

y la instancia de la clase Persona:

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

puedes actualizar zipCode of raj con:

 import com.softwaremill.quicklens._ val updatedRaj = raj.modify(_.address.zipCode).using(_ + 1) 
Intereting Posts