İç içe geçmiş yapıları güncellemenin daha temiz yolu


124

Aşağıdaki iki case classesere sahip olduğumu söyle :

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

ve aşağıdaki Personsınıf örneği :

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

Ben güncelleme isteyip istemediğim zipCodeait rajo zaman yapmak zorunda olacak:

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

Daha fazla iç içe geçme seviyesi ile bu daha da çirkinleşir. Bu update-intür iç içe geçmiş yapıları güncellemenin daha temiz bir yolu (Clojure's gibi ) var mı?


1
Değişmezliği korumak istediğinizi varsayıyorum, aksi takdirde Kişilerin adres beyanının önüne bir değişken yapıştırın.
GClaramunt

8
@GClaramunt: Evet, değişmezliği korumak istiyorum.
missingfaktor

Yanıtlar:


94

fermuarlar

Huet's Zipper , değişmez bir veri yapısının uygun geçişini ve 'mutasyonunu' sağlar. Scalaz, Stream( scalaz.Zipper ) ve Tree( scalaz.TreeLoc ) için Fermuarlar sağlar . Fermuarın yapısının, bir cebirsel ifadenin sembolik farklılaşmasına benzeyen bir şekilde, orijinal veri yapısından otomatik olarak türetilebildiği ortaya çıktı.

Peki bu, Scala vaka sınıflarınızda size nasıl yardımcı olur? Eh, Lukas Rytz kısa süre önce , açıklamalı vaka sınıfları için otomatik olarak fermuarlar oluşturan bir scalac uzantısının prototipini yaptı . Onun örneğini burada tekrar edeceğim:

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)

Bu nedenle, topluluğun Scala ekibini bu çabanın sürdürülmesi ve derleyiciye entegre edilmesi gerektiğine ikna etmesi gerekir.

Bu arada, Lukas kısa süre önce bir DSL aracılığıyla kullanıcı tarafından programlanabilen bir Pacman sürümü yayınladı . Bununla birlikte, herhangi bir @zipek açıklama göremediğim için değiştirilmiş derleyiciyi kullanmış gibi görünmüyor .

Ağacı Yeniden Yazma

Diğer durumlarda, bazı stratejilere göre (yukarıdan aşağıya, aşağıdan yukarıya) ve yapının bir noktasında değerle eşleşen kurallara göre tüm veri yapısı boyunca bir miktar dönüşüm uygulamak isteyebilirsiniz. Klasik örnek, belki de bilgiyi değerlendirmek, basitleştirmek veya toplamak için bir AST'yi dil için dönüştürmektir. Kiama , Yeniden Yazmayı destekler, Yeniden Yazım Testlerindeki örneklere bakın ve bu videoyu izleyin . İşte iştahınızı kabartacak bir pasaj:

// 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'nın bunu başarmak için tip sisteminin dışına çıktığını unutmayın .


2
Taahhüt arayanlar için. İşte burada: github.com/soundrabbit/scala/commit/… (Sanırım ..)
IttayD

15
Hey, lensler nerede?
Daniel C. Sobral

Bu problemle yeni karşılaştım ve @zip fikri kulağa gerçekten harika geliyor, belki de tüm vaka sınıflarının sahip olduğu kadar ileri götürülmeli? Bu neden uygulanmıyor? Lensler güzeldir, ancak büyük ve birçok sınıf / vaka sınıfında, sadece bir ayarlayıcı istiyorsanız ve bir artırıcı gibi süslü bir şey istemiyorsanız, sadece standart bir şablon.
Johan S

186

Bu tür şeyler için yapıldıkları için kimsenin lens eklememesi komik. Yani, burada üzerinde bir CS arka plan kağıdı olup burada lensler üzerinde dokunmatik kısaca Scala kullanmak bir blog, burada Scalaz için lensler uygulamasıdır ve burada şaşırtıcı sorunuza benziyor onu kullanarak bazı kod vardır. Ve kazan plakasını kısaltmak için, işte vaka sınıfları için Scalaz lensleri üreten bir eklenti.

Bonus puanlar için, işte lenslere değinen başka bir SO sorusu ve Tony Morris'in yazdığı bir makale .

Lenslerle ilgili en önemli şey, bir araya getirilebilir olmalarıdır. Bu yüzden ilk başta biraz hantaldırlar, ancak onları kullandıkça yer kazanmaya devam ederler. Ayrıca, test edilebilirlik için harikadırlar, çünkü yalnızca tek tek lensleri test etmeniz gerekir ve kompozisyonlarını kabul edebilirsiniz.

Öyleyse, bu cevabın sonunda sağlanan bir uygulamaya göre, işte bunu lenslerle nasıl yapacağınız. İlk olarak, bir adresteki bir posta kodunu ve bir kişideki bir adresi değiştirmek için lensleri beyan edin:

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

Şimdi, bir kişinin posta kodunu değiştiren bir mercek elde etmek için onları oluşturun:

val personZipCodeLens = personAddressLens andThen addressZipCodeLens

Son olarak, rajı değiştirmek için bu merceği kullanın:

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

Veya biraz sözdizimsel şeker kullanarak:

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

Ya da:

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

İşte bu örnek için kullanılan, Scalaz'dan alınan basit uygulama:

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'in lens eklentisinin bir açıklamasıyla bu yanıtı güncellemek isteyebilirsiniz.
missingfaktor

@missingfaktor Elbette. Bağlantı? Böyle bir eklentinin farkında değildim.
Daniel C. Sobral

1
Kod personZipCodeLens.set(raj, personZipCodeLens.get(raj) + 1)ile aynıpersonZipCodeLens mod (raj, _ + 1)
ron

@ron mod, lensler için ilkel değildir.
Daniel C. Sobral

Tony Morris konuyla ilgili harika bir makale yazdı . Bence cevabında onu bağlamalısın.
lostfaktor

11

Lensleri kullanmak için faydalı araçlar:

Sadece bu eklemek istediğiniz Makrokozmos ve Rillit Scala 2.10 makro dayalı projeler, Dinamik Mercek Creation sağlar.


Rillit Kullanımı:

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

Makrokozmos Kullanımı:

Bu, geçerli derleme çalıştırmasında tanımlanan durum sınıfları için bile işe yarar.

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

Muhtemelen daha da iyi olan Rillit'i kaçırdınız. :-) github.com/akisaarinen/rillit
missingfaktor

Güzel, kontrol edeceğim
Sebastien Lorber

1
Btw ben Rillit dahil etmek benim cevap düzenlendiğini ancak Rillit iyidir Gerçekten neden anlamıyorum, ilk görme @missingfaktor aynı Detayının aynı işlevselliği sağlamak gibi görünüyor
Sebastien Lorber

@SebastienLorber Eğlenceli gerçek: Rillit Fince ve Lens anlamına gelir :)
Kai Sellgren

Hem Macrocosm hem de Rillit son 4 yılda güncellenmemiş görünüyor.
Erik van Oosten

9

En güzel sözdizimine ve en iyi işlevselliğe sahip olan Scala kitaplığının ve burada bahsedilmeyen bir kitaplığın monocle olduğunu araştırıyorum ki bu benim için gerçekten iyi oldu. Bir örnek aşağıdaki gibidir:

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

Bunlar çok güzel ve lensleri birleştirmenin birçok yolu var. Örneğin Scalaz, çok sayıda standart şablon gerektirir ve bu hızlı derlenir ve harika çalışır.

Bunları projenizde kullanmak için, bunu bağımlılıklarınıza eklemeniz yeterlidir:

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

Shapeless hile yapar:

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

ile:

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

Buradaki bazı diğer yanıtlar, belirli bir yapının daha derine inmek için lensler oluşturmanıza izin verirken, bu şekilsiz lensler (ve diğer kitaplıklar / makrolar), iki ilgisiz lensi birleştirmenize izin verir, böylece rastgele sayıda parametreyi rastgele konumlara ayarlayan lens yapabilirsiniz. yapınızda. Karmaşık veri yapıları için bu ek kompozisyon çok faydalıdır.


Sonunda LensDaniel C. Sobral'ın cevabındaki kodu kullandım ve bu nedenle harici bir bağımlılık eklemekten kaçındım.
simbo1905

7

Birleştirilebilir doğası gereği lensler, yoğun şekilde iç içe geçmiş yapılar sorununa çok güzel bir çözüm sunar. Bununla birlikte, düşük seviyede iç içe geçme ile, bazen lenslerin biraz fazla olduğunu hissediyorum ve iç içe güncellemelerin olduğu yalnızca birkaç yer varsa, tüm lens yaklaşımını tanıtmak istemiyorum. Bütünlük adına, işte bu durum için çok basit / pragmatik bir çözüm:

Yaptığım şey, modify...üst düzey yapıya, çirkin iç içe geçmiş kopya ile ilgilenen birkaç yardımcı işlev yazmaktır . Örneğin:

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

Ana hedefim (istemci tarafında güncellemeyi basitleştirmek) şu şekildedir:

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

Değişiklik yardımcılarının tam setini oluşturmak açıkça sinir bozucu. Ancak dahili şeyler için, belirli bir iç içe geçmiş alanı ilk kez değiştirmeye çalıştığınızda bunları oluşturmanız genellikle uygundur.


4

Belki QuickLens , sorunuza daha iyi uymaktadır . QuickLens, bir IDE uyumlu ifadeyi orijinal kopya ifadesine yakın bir şeye dönüştürmek için makroları kullanır.

İki örnek vaka sınıfı verildiğinde:

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

ve Kişi sınıfının örneği:

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

raj'ın zipCode'unu şu şekilde güncelleyebilirsiniz:

import com.softwaremill.quicklens._
val updatedRaj = raj.modify(_.address.zipCode).using(_ + 1)
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.