Cách sạch hơn để cập nhật các cấu trúc lồng nhau


124

Nói rằng tôi đã có hai case classes sau:

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

và ví dụ sau của Personlớp:

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

Bây giờ nếu tôi muốn cập nhật zipCodecủa rajsau đó tôi sẽ phải làm:

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

Với nhiều cấp độ lồng nhau, điều này thậm chí còn xấu hơn. Có cách nào sạch hơn (giống như Clojure update-in) để cập nhật các cấu trúc lồng nhau như vậy không?


1
Tôi giả sử bạn muốn giữ gìn sự bất biến, nếu không, chỉ cần dán một var trước phần khai báo địa chỉ của Người.
GClaramunt

8
@GClaramunt: Vâng, tôi muốn giữ gìn sự bất biến.
missingfaktor

Câu trả lời:


94

Khóa kéo

Zipper của Huet cung cấp khả năng truyền tải thuận tiện và "đột biến" của cấu trúc dữ liệu bất biến. Scalaz cung cấp Khóa kéo cho Stream( scalaz.Zipper ) và Tree( scalaz.TreeLoc ). Nó chỉ ra rằng cấu trúc của dây kéo có thể tự động lấy được từ cấu trúc dữ liệu ban đầu, theo cách tương tự như sự phân biệt biểu tượng của một biểu thức đại số.

Nhưng làm thế nào điều này giúp bạn với các lớp trường hợp Scala của bạn? Chà, gần đây Lukas Rytz đã tạo ra một phần mở rộng cho scalac sẽ tự động tạo khóa kéo cho các lớp trường hợp chú thích. Tôi sẽ tái tạo ví dụ của anh ấy ở đây:

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)

Vì vậy, cộng đồng cần thuyết phục nhóm Scala rằng nỗ lực này cần được tiếp tục và tích hợp vào trình biên dịch.

Tình cờ, Lukas gần đây đã xuất bản một phiên bản Pacman, người dùng có thể lập trình thông qua DSL. Tuy nhiên, có vẻ như anh ta không sử dụng trình biên dịch đã sửa đổi, vì tôi không thể thấy bất kỳ @zipchú thích nào .

Viết lại cây

Trong các trường hợp khác, bạn có thể muốn áp dụng một số chuyển đổi trên toàn bộ cấu trúc dữ liệu, theo một số chiến lược (từ trên xuống, từ dưới lên) và dựa trên các quy tắc khớp với giá trị tại một số điểm trong cấu trúc. Ví dụ cổ điển là chuyển đổi AST cho một ngôn ngữ, có lẽ để đánh giá, đơn giản hóa hoặc thu thập thông tin. Kiama hỗ trợ Viết lại , xem các ví dụ trong RewriterTests và xem video này . Đây là một đoạn để kích thích sự thèm ăn của bạn:

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

Lưu ý rằng Kiama bước ra ngoài hệ thống loại để đạt được điều này.


2
Đối với những người tìm kiếm các cam kết. Đây là: github.com/soundrabbit/scala/commit/, (tôi nghĩ ..)
IttayD

15
Này, ống kính ở đâu?
Daniel C. Sobral

Tôi vừa gặp phải vấn đề này và ý tưởng @zip nghe có vẻ rất tuyệt vời, có lẽ nó nên được thực hiện cho đến nay mà tất cả các lớp trường hợp có nó? Tại sao điều này không được thực hiện? Ống kính là tốt nhưng với lớn và nhiều lớp / lớp trường hợp, nó chỉ là nồi hơi nếu bạn chỉ muốn một setter và không có gì lạ mắt như một bộ tăng.
Johan S

186

Thật buồn cười là không ai thêm ống kính, vì họ đã MADE cho loại công cụ này. Vì vậy, đây là một bài viết nền CS về nó, đây là một blog chạm nhanh vào các ống kính sử dụng trong Scala, đây là một triển khai ống kính cho Scalaz và đây là một số mã sử dụng nó, trông giống như câu hỏi của bạn. Và, để cắt giảm tấm nồi hơi, đây là một plugin tạo ống kính Scalaz cho các lớp tình huống.

Để nhận điểm thưởng, đây là một câu hỏi SO khác chạm vào ống kính và một bài báo của Tony Morris.

Vấn đề lớn về ống kính là chúng có thể ghép lại được. Vì vậy, ban đầu chúng hơi cồng kềnh, nhưng chúng tiếp tục đạt được nhiều điểm hơn khi bạn sử dụng chúng. Ngoài ra, chúng rất tốt cho khả năng kiểm tra, vì bạn chỉ cần kiểm tra từng ống kính riêng lẻ và có thể được cấp thành phần của chúng.

Vì vậy, dựa trên việc triển khai được cung cấp ở cuối câu trả lời này, đây là cách bạn thực hiện với ống kính. Đầu tiên, khai báo các ống kính để thay đổi mã zip trong một địa chỉ và một địa chỉ trong một người:

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

Bây giờ, hãy soạn chúng để có một ống kính thay đổi mã zip ở một người:

val personZipCodeLens = personAddressLens andThen addressZipCodeLens

Cuối cùng, sử dụng ống kính đó để thay đổi raj:

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

Hoặc, sử dụng một số đường cú pháp:

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

Hoặc thậm chí:

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

Đây là cách thực hiện đơn giản, được lấy từ Scalaz, được sử dụng cho ví dụ này:

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
Bạn có thể muốn cập nhật câu trả lời này với một mô tả về plugin ống kính của Gerolf Seitz.
missingfaktor

@missingfaktor Chắc chắn rồi. Liên kết? Tôi đã không nhận thức được các plugin như vậy.
Daniel C. Sobral

1
personZipCodeLens.set(raj, personZipCodeLens.get(raj) + 1)này giống nhưpersonZipCodeLens mod (raj, _ + 1)
ron

@ron modkhông phải là một nguyên thủy cho ống kính, mặc dù.
Daniel C. Sobral

Tony Morris đã viết một bài báo tuyệt vời về chủ đề này. Tôi nghĩ bạn nên liên kết nó trong câu trả lời của bạn.
missingfaktor

11

Các công cụ hữu ích để sử dụng Ống kính:

Chỉ muốn thêm rằng thế giới vĩ môRillit dự án, dựa trên Scala 2.10 macro, cung cấp động Lens Creation.


Sử dụng 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))

Sử dụng Macrocosm:

Điều này thậm chí hoạt động cho các lớp trường hợp được xác định trong chạy biên dịch hiện tại.

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

Bạn có thể đã bỏ lỡ Rillit, thậm chí còn tốt hơn. :-) github.com/akisaarinen/rillit
missingfaktor

Hay đấy, sẽ kiểm tra xem
Sebastien Lorber

1
Btw Tôi đã chỉnh sửa câu trả lời của mình để bao gồm Rillit nhưng tôi không thực sự hiểu tại sao Rillit tốt hơn, họ dường như cung cấp chức năng tương tự trong cùng một chi tiết ngay từ cái nhìn đầu tiên @missingfaktor
Sebastien Lorber

@SebastienLorber Sự thật thú vị: Rillit là tiếng Phần Lan và có nghĩa là Ống kính :)
Kai Sellgren

Cả Macrocosm và Rillit dường như không được cập nhật trong 4 năm qua.
Erik van Oosten

9

Tôi đã tìm kiếm xung quanh những gì thư viện Scala có cú pháp đẹp nhất và chức năng tốt nhất và một thư viện không được đề cập ở đây là monocle mà đối với tôi đã thực sự tốt. Một ví dụ sau:

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

Đây là rất tốt đẹp và có nhiều cách để kết hợp các ống kính. Ví dụ, Scalaz đòi hỏi rất nhiều nồi hơi và điều này biên dịch nhanh và chạy tuyệt vời.

Để sử dụng chúng trong dự án của bạn, chỉ cần thêm phần này vào phần phụ thuộc của bạn:

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

Shapless thực hiện các mẹo:

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

với:

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

Lưu ý rằng trong khi một số câu trả lời khác ở đây cho phép bạn kết hợp các ống kính để đi sâu hơn vào cấu trúc nhất định thì các ống kính có hình dạng này (và các thư viện / macro khác) cho phép bạn kết hợp hai ống kính không liên quan để bạn có thể đặt ống kính đặt số lượng thông số tùy ý vào các vị trí tùy ý trong cấu trúc của bạn. Đối với các cấu trúc dữ liệu phức tạp mà thành phần bổ sung là rất hữu ích.


Lưu ý rằng cuối cùng tôi đã sử dụng Lensmã trong câu trả lời của Daniel C. Sobral và do đó tránh thêm một phụ thuộc bên ngoài.
simbo1905

7

Do tính chất có thể kết hợp của chúng, ống kính cung cấp một giải pháp rất hay cho vấn đề cấu trúc được lồng rất nhiều. Tuy nhiên, với mức độ lồng thấp, đôi khi tôi cảm thấy ống kính hơi quá nhiều và tôi không muốn giới thiệu toàn bộ cách tiếp cận ống kính nếu chỉ có một vài nơi có cập nhật lồng nhau. Để hoàn thiện, đây là một giải pháp rất đơn giản / thực dụng cho trường hợp này:

Những gì tôi làm chỉ đơn giản là viết một vài modify...hàm trợ giúp trong cấu trúc cấp cao nhất, xử lý các bản sao lồng nhau xấu xí. Ví dụ:

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

Mục tiêu chính của tôi (đơn giản hóa việc cập nhật về phía khách hàng) đã đạt được:

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

Tạo toàn bộ bộ trợ giúp sửa đổi rõ ràng là khó chịu. Nhưng đối với các công cụ nội bộ, thường chỉ cần tạo chúng trong lần đầu tiên bạn cố gắng sửa đổi một trường lồng nhau nhất định.


4

Có lẽ QuickLens phù hợp với câu hỏi của bạn tốt hơn. QuickLens sử dụng macro để chuyển đổi một biểu thức thân thiện với IDE thành một biểu thức gần với câu lệnh sao chép gốc.

Cho hai lớp trường hợp ví dụ:

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

và thể hiện của lớp Person:

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

bạn có thể cập nhật mã zip của raj với:

import com.softwaremill.quicklens._
val updatedRaj = raj.modify(_.address.zipCode).using(_ + 1)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.