Cara lebih bersih untuk memperbarui struktur bersarang


124

Katakanlah saya sudah mengikuti dua case classes:

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

dan instance Personkelas berikut :

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

Sekarang jika saya ingin memperbarui zipCodedari rajmaka saya akan harus melakukan:

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

Dengan lebih banyak tingkat bersarang ini menjadi lebih buruk. Apakah ada cara yang lebih bersih (seperti milik Clojure update-in) untuk memperbarui struktur bersarang seperti itu?


1
Saya berasumsi Anda ingin mempertahankan ketidakmampuan, jika tidak, hanya menempelkan var di depan deklarasi alamat Orang.
GClaramunt

8
@GClaramunt: Ya, saya ingin mempertahankan keabadian.
missingfaktor

Jawaban:


94

Resleting

Huet's Zipper memberikan traversal yang nyaman dan 'mutasi' dari struktur data yang tidak berubah. Scalaz menyediakan Ritsleting untuk Stream( scalaz.Zipper ), dan Tree( scalaz.TreeLoc ). Ternyata struktur ritsleting secara otomatis dapat diturunkan dari struktur data asli, dengan cara yang menyerupai diferensiasi simbolis dari ekspresi aljabar.

Tetapi bagaimana ini membantu Anda dengan kelas kasus Scala Anda? Nah, Lukas Rytz baru-baru ini membuat prototipe ekstensi untuk scalac yang secara otomatis akan membuat ritsleting untuk kelas kasus beranotasi. Saya akan mereproduksi contohnya di sini:

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)

Jadi masyarakat perlu meyakinkan tim Scala bahwa upaya ini harus dilanjutkan dan diintegrasikan ke dalam penyusun.

Kebetulan, Lukas baru-baru ini menerbitkan versi Pacman, pengguna yang dapat diprogram melalui DSL. Namun, sepertinya dia tidak menggunakan kompiler yang dimodifikasi, karena saya tidak dapat melihat @zipanotasi.

Penulisan Ulang Pohon

Dalam keadaan lain, Anda mungkin ingin menerapkan beberapa transformasi di seluruh struktur data, sesuai dengan beberapa strategi (top-down, bottom-up), dan berdasarkan aturan yang cocok dengan nilai di beberapa titik dalam struktur. Contoh klasik adalah mengubah AST untuk bahasa, mungkin untuk mengevaluasi, menyederhanakan, atau mengumpulkan informasi. Kiama mendukung Penulisan Ulang , lihat contoh di RewriterTests , dan tonton video ini . Berikut cuplikan untuk membangkitkan selera Anda:

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

Perhatikan bahwa Kiama melangkah keluar dari sistem tipe untuk mencapai ini.


2
Bagi mereka yang mencari komit. Ini dia: github.com/soundrabbit/scala/commit/… (saya pikir ..)
IttayD

15
Hei, dimana lensanya?
Daniel C. Sobral

Saya baru saja mengalami masalah ini dan ide @zip terdengar sangat fantastis, mungkin harus diambil sejauh semua kelas kasus memilikinya? Mengapa ini tidak diterapkan? Lensa bagus tetapi dengan kelas besar dan banyak / kelas kasus hanya boilerplate jika Anda hanya ingin setter dan tidak ada yang mewah seperti incrementer.
Johan S

186

Lucu tidak ada yang menambahkan lensa, karena mereka DIBUAT untuk hal semacam ini. Jadi, di sini adalah kertas latar belakang CS di atasnya, di sini adalah blog yang menyentuh sebentar tentang penggunaan lensa di Scala, di sini adalah implementasi lensa untuk Scalaz dan di sini ada beberapa kode yang menggunakannya, yang secara mengejutkan tampak seperti pertanyaan Anda. Dan, untuk mengurangi pelat ketel, inilah plugin yang menghasilkan lensa Scalaz untuk kelas kasus.

Untuk poin bonus, inilah pertanyaan SO lainnya yang menyentuh lensa, dan kertas karya Tony Morris.

Masalah besar tentang lensa adalah mereka dapat dikomposit. Jadi mereka agak rumit pada awalnya, tetapi mereka terus mendapatkan tanah semakin Anda menggunakannya. Selain itu, lensa ini sangat bagus untuk pengujian, karena Anda hanya perlu menguji masing-masing lensa, dan dapat menerima begitu saja komposisi mereka.

Jadi, berdasarkan implementasi yang disediakan di akhir jawaban ini, inilah cara Anda melakukannya dengan lensa. Pertama, nyatakan lensa untuk mengubah kode pos di alamat, dan alamat di seseorang:

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

Sekarang, buat mereka untuk mendapatkan lensa yang mengubah kode pos dalam diri seseorang:

val personZipCodeLens = personAddressLens andThen addressZipCodeLens

Akhirnya, gunakan lensa itu untuk mengubah raj:

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

Atau, menggunakan gula sintaksis:

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

Atau bahkan:

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

Inilah implementasi sederhana, diambil dari Scalaz, digunakan untuk contoh ini:

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
}

1
Anda mungkin ingin memperbarui jawaban ini dengan deskripsi plugin lensa Gerolf Seitz.
missingfaktor

@missingfaktor Tentu. Tautan? Saya tidak mengetahui plugin semacam itu.
Daniel C. Sobral

1
Kode personZipCodeLens.set(raj, personZipCodeLens.get(raj) + 1)ini sama denganpersonZipCodeLens mod (raj, _ + 1)
ron

@ron modbukan primitif untuk lensa.
Daniel C. Sobral

Tony Morris telah menulis makalah yang bagus tentang masalah ini. Saya pikir Anda harus menautkannya dalam jawaban Anda.
missingfaktor

11

Alat yang berguna untuk menggunakan Lensa:

Hanya ingin menambahkan bahwa makrokosmos dan Rillit proyek, berdasarkan Scala 2.10 macro, menyediakan Dinamis Penciptaan Lensa.


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

Menggunakan Macrocosm:

Ini bahkan bekerja untuk kelas kasus yang didefinisikan dalam menjalankan kompilasi saat ini.

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

Anda mungkin melewatkan Rillit yang bahkan lebih baik. :-) github.com/akisaarinen/rillit
missingfaktor

Bagus, akan memeriksa itu
Sebastien Lorber

1
Btw saya mengedit jawaban saya untuk memasukkan Rillit tetapi saya tidak benar-benar mengerti mengapa Rillit lebih baik, mereka tampaknya menyediakan fungsionalitas yang sama dalam verboseness yang sama pada pandangan pertama @missingfaktor
Sebastien Lorber

@SebastienLorber Fakta menyenangkan: Rillit adalah bahasa Finlandia dan berarti Lensa :)
Kai Sellgren

Baik Macrocosm dan Rillit tampaknya tidak diperbarui dalam 4 tahun terakhir.
Erik van Oosten

9

Saya telah mencari-cari perpustakaan Scala yang memiliki sintaksis terbaik dan fungsi terbaik dan satu perpustakaan yang tidak disebutkan di sini adalah monocle yang bagi saya sangat bagus. Contoh berikut:

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

Ini sangat bagus dan ada banyak cara untuk menggabungkan lensa. Scalaz misalnya membutuhkan banyak boilerplate dan ini mengkompilasi dengan cepat dan berjalan dengan baik.

Untuk menggunakannya dalam proyek Anda cukup tambahkan ini ke dependensi Anda:

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
)

7

Tak berbentuk melakukan trik:

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

dengan:

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

Perhatikan bahwa sementara beberapa jawaban lain di sini memungkinkan Anda menyusun lensa untuk masuk lebih dalam ke dalam struktur yang diberikan lensa tak berbentuk ini (dan perpustakaan / makro lain) memungkinkan Anda menggabungkan dua lensa yang tidak terkait sehingga Anda dapat membuat lensa yang menetapkan jumlah parameter yang berubah-ubah menjadi posisi acak. dalam struktur Anda. Untuk struktur data yang kompleks komposisi tambahan itu sangat membantu.


Perhatikan bahwa saya akhirnya menggunakan Lenskode dalam jawaban Daniel C. Sobral dan karenanya menghindari menambahkan ketergantungan eksternal.
simbo1905

7

Karena sifat komposisinya, lensa memberikan solusi yang sangat bagus untuk masalah struktur bersarang. Namun dengan tingkat sarang yang rendah, saya terkadang merasa lensa terlalu banyak, dan saya tidak ingin memperkenalkan pendekatan seluruh lensa jika hanya ada beberapa tempat dengan pembaruan bersarang. Demi kelengkapan, berikut adalah solusi yang sangat sederhana / pragmatis untuk kasus ini:

Apa yang saya lakukan adalah hanya menulis beberapa modify...fungsi pembantu di struktur tingkat atas, yang berurusan dengan salinan bersarang jelek. Misalnya:

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

Tujuan utama saya (menyederhanakan pembaruan di sisi klien) tercapai:

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

Membuat set lengkap pembantu modifikasi jelas menjengkelkan. Tetapi untuk hal-hal internal seringkali cukup baik untuk membuatnya saat pertama kali Anda mencoba memodifikasi bidang bersarang tertentu.


4

Mungkin QuickLens cocok dengan pertanyaan Anda dengan lebih baik. QuickLens menggunakan makro untuk mengubah ekspresi ramah IDE menjadi sesuatu yang dekat dengan pernyataan salin asli.

Diberikan dua contoh kelas kasus:

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

dan instance dari kelas Person:

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

Anda dapat memperbarui kode pos raj dengan:

import com.softwaremill.quicklens._
val updatedRaj = raj.modify(_.address.zipCode).using(_ + 1)
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.