วิธีที่สะอาดกว่าในการอัปเดตโครงสร้างที่ซ้อนกัน


124

สมมติว่าฉันมีสองสิ่งต่อไปนี้case class:

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

และตัวอย่างต่อไปนี้ของPersonคลาส:

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

ตอนนี้ถ้าผมต้องการที่จะปรับปรุงzipCodeของrajแล้วฉันจะต้องทำ:

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

ด้วยระดับที่มากขึ้นของการซ้อนสิ่งนี้จะทำให้น่าเกลียดยิ่งขึ้น มีวิธีที่สะอาดกว่า (เช่น Clojure's update-in) ในการอัปเดตโครงสร้างที่ซ้อนกันดังกล่าวหรือไม่?


1
ฉันคิดว่าคุณต้องการรักษาความไม่แน่นอนมิฉะนั้นให้ติด var หน้าการประกาศที่อยู่ของบุคคล
GClaramunt

8
@GClaramunt: ใช่ฉันต้องการรักษาความไม่เปลี่ยนรูป
missingfaktor

คำตอบ:


94

รูดซิป

Huet's Zipperให้การเคลื่อนผ่านที่สะดวกและ 'การกลายพันธุ์' ของโครงสร้างข้อมูลที่ไม่เปลี่ยนรูป Scalaz ให้ Zippers สำหรับStream( scalaz.Zipper ) และTree( scalaz.TreeLoc ) ปรากฎว่าโครงสร้างของซิปนั้นได้มาจากโครงสร้างข้อมูลดั้งเดิมโดยอัตโนมัติในลักษณะที่คล้ายกับความแตกต่างเชิงสัญลักษณ์ของนิพจน์พีชคณิต

แต่สิ่งนี้จะช่วยคุณในชั้นเรียนกรณี Scala ของคุณได้อย่างไร เมื่อเร็ว ๆ นี้ Lukas Rytz ได้สร้างต้นแบบส่วนขยายให้กับ scalac ซึ่งจะสร้างซิปสำหรับคลาสเคสที่มีคำอธิบายประกอบโดยอัตโนมัติ ฉันจะทำซ้ำตัวอย่างของเขาที่นี่:

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)

ดังนั้นชุมชนจึงจำเป็นต้องชักชวนทีม Scala ว่าควรดำเนินการต่อและรวมเข้ากับคอมไพเลอร์

บังเอิญเมื่อไม่นานมานี้ Lukas ได้เผยแพร่เวอร์ชัน Pacman ซึ่งผู้ใช้สามารถตั้งโปรแกรมผ่าน DSL ได้ ดูเหมือนว่าเขาใช้คอมไพเลอร์ที่แก้ไขไม่ได้เพราะฉันไม่เห็น@zipคำอธิบายประกอบใด ๆ

การเขียนต้นไม้ใหม่

ในกรณีอื่น ๆ คุณอาจต้องการใช้การเปลี่ยนแปลงบางอย่างกับโครงสร้างข้อมูลทั้งหมดตามกลยุทธ์บางอย่าง (จากบนลงล่างขึ้นล่าง) และอิงตามกฎที่ตรงกับค่า ณ จุดใดจุดหนึ่งในโครงสร้าง ตัวอย่างคลาสสิกคือการเปลี่ยน AST สำหรับภาษาบางทีเพื่อประเมินลดความซับซ้อนหรือรวบรวมข้อมูล KiamaรองรับRewritingดูตัวอย่างในRewriterTestsและดูวิดีโอนี้ นี่คือตัวอย่างเพื่อกระตุ้นความอยากอาหารของคุณ:

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

โปรดทราบว่า Kiama ก้าวออกไปนอกระบบประเภทเพื่อให้บรรลุสิ่งนี้


2
สำหรับผู้ที่มองหาการกระทำ นี่ไง: github.com/soundrabbit/scala/commit/… (ผมว่า .. )
IttayD

15
เฮ้ยเลนส์อยู่ไหน
Daniel C. Sobral

ฉันเพิ่งพบปัญหานี้และความคิด @zip ฟังดูยอดเยี่ยมจริงๆบางทีมันควรจะใช้จนถึงตอนนี้ในทุกกรณีหรือไม่ เหตุใดจึงไม่มีการใช้งาน เลนส์เป็นสิ่งที่ดี แต่มีคลาส / เคสขนาดใหญ่และจำนวนมากมันเป็นเพียงต้นแบบถ้าคุณต้องการตัวเซ็ตเตอร์และไม่มีอะไรที่น่าสนใจเหมือนตัวเพิ่มค่า
Johan S

186

ตลกดีที่ไม่มีใครเพิ่มเลนส์เพราะมันถูกสร้างมาเพื่อของแบบนี้ ดังนั้นนี่คือกระดาษพื้นหลัง CS ด้านบนนี่คือบล็อกที่สัมผัสสั้น ๆ เกี่ยวกับเลนส์ที่ใช้ใน Scala นี่คือการใช้งานเลนส์สำหรับ Scalaz และนี่คือรหัสบางส่วนที่ใช้ซึ่งดูเหมือนคำถามของคุณอย่างน่าประหลาดใจ และเพื่อลดแผ่นหม้อต้มนี่คือปลั๊กอินที่สร้างเลนส์ Scalaz สำหรับคลาสเคส

สำหรับคะแนนโบนัสนี่คือคำถามอื่น ๆ ที่เกี่ยวข้องกับเลนส์และกระดาษของ Tony Morris

เรื่องใหญ่เกี่ยวกับเลนส์คือสามารถประกอบได้ ดังนั้นพวกเขาจึงค่อนข้างยุ่งยากในตอนแรก แต่พวกเขาก็ยังคงได้รับประโยชน์ยิ่งคุณใช้มันมากขึ้น นอกจากนี้ยังเหมาะสำหรับการทดสอบเนื่องจากคุณต้องทดสอบเลนส์แต่ละชิ้นเท่านั้นและสามารถพิจารณาองค์ประกอบได้

ดังนั้นจากการใช้งานที่ให้ไว้ในตอนท้ายของคำตอบนี้คุณจะใช้เลนส์ได้อย่างไร ขั้นแรกประกาศเลนส์เพื่อเปลี่ยนรหัสไปรษณีย์ในที่อยู่และที่อยู่ในตัวบุคคล:

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

ตอนนี้เขียนมันเพื่อให้ได้เลนส์ที่เปลี่ยนรหัสไปรษณีย์ในตัวบุคคล:

val personZipCodeLens = personAddressLens andThen addressZipCodeLens

สุดท้ายใช้เลนส์นั้นเพื่อเปลี่ยน raj:

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

หรือใช้น้ำตาลวากยสัมพันธ์:

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

หรือแม้กระทั่ง:

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

นี่คือการใช้งานง่าย ๆ ซึ่งนำมาจาก Scalaz ซึ่งใช้สำหรับตัวอย่างนี้:

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
คุณอาจต้องการอัปเดตคำตอบนี้ด้วยคำอธิบายของปลั๊กอินเลนส์ของ Gerolf Seitz
missingfaktor

@missingfaktor ชัวร์. Link? ฉันไม่ทราบถึงปลั๊กอินดังกล่าว
Daniel C. Sobral

1
รหัสpersonZipCodeLens.set(raj, personZipCodeLens.get(raj) + 1)เหมือนกับpersonZipCodeLens mod (raj, _ + 1)
ron

@ron modไม่ใช่วัตถุดั้งเดิมสำหรับเลนส์แม้ว่า
Daniel C. Sobral

Tony Morris ได้เขียนบทความที่ยอดเยี่ยมเกี่ยวกับเรื่องนี้ ฉันคิดว่าคุณควรเชื่อมโยงในคำตอบของคุณ
missingfaktor

11

เครื่องมือที่มีประโยชน์ในการใช้เลนส์:

เพียงแค่ต้องการเพิ่มว่าโครงการMacrocosmและRillitซึ่งใช้มาโคร Scala 2.10 ให้การสร้างเลนส์แบบไดนามิก


ใช้ 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))

ใช้ Macrocosm:

สิ่งนี้ใช้ได้กับคลาสเคสที่กำหนดไว้ในการรันคอมไพล์ปัจจุบัน

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

คุณอาจพลาด Rillit ซึ่งดีกว่า :-) github.com/akisaarinen/rillit
missingfaktor

Nice จะตรวจสอบว่า
Sebastien Lorber

1
Btw ฉันแก้ไขคำตอบของฉันเพื่อรวม Rillit แต่ฉันไม่เข้าใจจริงๆว่าทำไม Rillit ถึงดีกว่าพวกเขาดูเหมือนจะให้การทำงานแบบเดียวกันในคำฟุ่มเฟือยเดียวกันตั้งแต่แรกเห็น @missingfaktor
Sebastien Lorber

@SebastienLorber Fun fact: Rillit เป็นภาษาฟินแลนด์และหมายถึงเลนส์ :)
Kai Sellgren

ทั้ง Macrocosm และ Rillit ดูเหมือนจะไม่ได้รับการอัปเดตในช่วง 4 ปีที่ผ่านมา
Erik van Oosten

9

ฉันมองหาว่าไลบรารี Scala ใดที่มีไวยากรณ์ที่ดีที่สุดและฟังก์ชันการทำงานที่ดีที่สุดและหนึ่งไลบรารีที่ไม่ได้กล่าวถึงในที่นี้คือmonocleซึ่งสำหรับฉันแล้วมันดีมาก ตัวอย่างดังต่อไปนี้:

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

สิ่งเหล่านี้ดีมากและมีหลายวิธีในการรวมเลนส์ ตัวอย่างเช่น Scalaz ต้องการแผ่นสำเร็จรูปจำนวนมากและสิ่งนี้รวบรวมได้รวดเร็วและทำงานได้ดี

หากต้องการใช้ในโครงการของคุณเพียงเพิ่มสิ่งนี้ในการอ้างอิงของคุณ:

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

เคล็ดลับไม่มีรูปร่าง:

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

ด้วย:

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

โปรดทราบว่าในขณะที่คำตอบอื่น ๆ ที่นี่ช่วยให้คุณสามารถสร้างเลนส์เพื่อเจาะลึกลงไปในโครงสร้างที่กำหนดเลนส์ที่ไม่มีรูปร่างเหล่านี้ (และไลบรารี / มาโครอื่น ๆ ) ช่วยให้คุณสามารถรวมเลนส์สองตัวที่ไม่เกี่ยวข้องกันเข้าด้วยกันเพื่อให้คุณสามารถสร้างเลนส์ที่กำหนดจำนวนพารามิเตอร์โดยพลการลงในตำแหน่งที่กำหนดเองได้ ในโครงสร้างของคุณ สำหรับโครงสร้างข้อมูลที่ซับซ้อนการจัดองค์ประกอบเพิ่มเติมมีประโยชน์มาก


โปรดทราบว่าในที่สุดฉันก็ใช้Lensรหัสในคำตอบของ Daniel C. Sobral และหลีกเลี่ยงการเพิ่มการพึ่งพาภายนอก
simbo1905

7

เนื่องจากลักษณะการประกอบเลนส์จึงเป็นทางออกที่ดีมากสำหรับปัญหาโครงสร้างที่ซ้อนกันมาก อย่างไรก็ตามด้วยการซ้อนในระดับต่ำบางครั้งฉันรู้สึกว่าเลนส์มีมากเกินไปและฉันไม่ต้องการแนะนำเลนส์ทั้งหมดหากมีเพียงไม่กี่แห่งที่มีการอัปเดตแบบซ้อนกัน เพื่อความสมบูรณ์นี่เป็นวิธีแก้ปัญหาที่ง่าย / ใช้ได้จริงสำหรับกรณีนี้:

สิ่งที่ฉันทำคือเพียงแค่เขียนmodify...ฟังก์ชันตัวช่วยสองสามอย่างในโครงสร้างระดับบนสุดซึ่งจัดการกับสำเนาที่ซ้อนกันน่าเกลียด ตัวอย่างเช่น:

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

เป้าหมายหลักของฉัน (การทำให้การอัปเดตง่ายขึ้นในฝั่งไคลเอ็นต์) สำเร็จ:

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

การสร้างตัวช่วยการปรับเปลี่ยนทั้งชุดเป็นเรื่องที่น่ารำคาญอย่างเห็นได้ชัด แต่สำหรับสิ่งของภายในมักจะเป็นเรื่องปกติที่จะสร้างขึ้นในครั้งแรกที่คุณพยายามแก้ไขช่องที่ซ้อนกันบางช่อง


4

บางทีQuickLensตรงกับคำถามของคุณมากกว่า QuickLens ใช้มาโครเพื่อแปลงนิพจน์ที่เป็นมิตรกับ IDE ให้เป็นสิ่งที่ใกล้เคียงกับคำสั่งคัดลอกต้นฉบับ

พิจารณากรณีตัวอย่างสองคลาส:

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

และตัวอย่างของคลาสบุคคล:

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

คุณสามารถอัปเดต zipCode ของ raj ด้วย:

import com.softwaremill.quicklens._
val updatedRaj = raj.modify(_.address.zipCode).using(_ + 1)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.