नेस्टेड संरचनाओं को अपडेट करने का स्वच्छ तरीका


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

घोंसले के अधिक स्तर के साथ यह और भी अधिक कुरूप हो जाता है। क्या update-inइस तरह के नेस्टेड संरचनाओं को अपडेट करने के लिए क्लीयर तरीके (क्लोजर की तरह कुछ ) है?


1
मुझे लगता है कि आप अपरिवर्तनीयता को संरक्षित करना चाहते हैं, अन्यथा, बस व्यक्तियों के पते की घोषणा के सामने एक संस्करण चिपका दें।
GClaramunt

8
@ जीलारमंट: हां, मैं अपरिवर्तनीयता को संरक्षित करना चाहता हूं।
missingfaktor

जवाबों:


94

ज़िपर

Huet's Zipper एक अपरिवर्तनीय डेटा संरचना के सुविधाजनक ट्रैवर्सल और 'म्यूटेशन' प्रदान करता है। Scalaz के लिए जिपर है प्रदान करता है Stream( scalaz.Zipper ), और Tree( scalaz.TreeLoc )। यह पता चला है कि जिपर की संरचना मूल डेटा संरचना से स्वचालित रूप से व्युत्पन्न है, एक तरीके से जो बीजीय अभिव्यक्ति के प्रतीकात्मक भेदभाव से मिलता जुलता है।

लेकिन यह आपके स्काला केस क्लासेस की मदद कैसे करता है? ठीक है, हाल ही में लुकास रिट्ज़ ने स्केलक के लिए एक एक्सटेंशन का प्रोटोटाइप बनाया जो स्वचालित रूप से एनोटेट केस क्लास के लिए ज़िपर बनाएगा। मैं उसका उदाहरण यहाँ दूंगा:

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)

इसलिए समुदाय को स्काला टीम को मनाने की जरूरत है कि इस प्रयास को जारी रखा जाए और इसे संकलक में एकीकृत किया जाए।

संयोग से, हाल ही में लुकास ने एक डीएसएल के माध्यम से उपयोगकर्ता द्वारा प्रोग्राम किए जाने वाले पचमैन का एक संस्करण प्रकाशित किया । ऐसा नहीं लगता कि उन्होंने संशोधित संकलक का उपयोग किया, हालांकि, जैसा कि मैं किसी भी @zipएनोटेशन को नहीं देख सकता ।

वृक्ष का पुनर्मिलन

अन्य परिस्थितियों में, आप कुछ रणनीति (टॉप-डाउन, बॉटम-अप) के अनुसार पूरे डेटा संरचना में कुछ परिवर्तन लागू करना पसंद कर सकते हैं, और संरचना के कुछ बिंदु पर मूल्य के खिलाफ मेल खाने वाले नियमों के आधार पर। शास्त्रीय उदाहरण एक भाषा के लिए एएसटी का रूपांतरण कर रहा है, शायद जानकारी का मूल्यांकन, सरलीकरण या एकत्र करने के लिए। 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
अरे, लेंस कहां हैं?
डैनियल सी। सोबरल

मुझे अभी इस समस्या का सामना करना पड़ा है और @ ज़िप विचार वास्तव में शानदार लगता है, शायद इसे अभी तक लिया जाना चाहिए कि सभी मामले वर्गों के पास हैं? इसे लागू क्यों नहीं किया गया? लेंस अच्छे हैं, लेकिन बड़े और कई वर्गों / केस वर्गों के साथ यह सिर्फ बॉयलरप्लेट है यदि आप बस एक सेटर चाहते हैं और एक इंक्रीज़र की तरह कुछ भी नहीं फैंसी।
जोहान एस

186

मजेदार है कि किसी ने लेंस नहीं जोड़ा, क्योंकि वे इस तरह के सामान के लिए MADE थे। तो, यहाँ एक CS बैकग्राउंड पेपर है, यहाँ एक ब्लॉग है, जो स्कैला में लेंस के उपयोग पर संक्षिप्त रूप से स्पर्श करता है, यहाँ Scalaz के लिए एक लेंस कार्यान्वयन है और यहाँ कुछ कोड का उपयोग किया गया है, जो आश्चर्यजनक रूप से आपके प्रश्न की तरह दिखता है। और, बॉयलर प्लेट पर कटौती करने के लिए, यहां एक प्लगइन है जो केस क्लासेस के लिए स्केलाज़ लेंस उत्पन्न करता है।

बोनस अंक के लिए, यहां एक और SO प्रश्न है जो लेंस पर स्पर्श करता है, और टोनी मॉरिस द्वारा एक पेपर

लेंस के बारे में बड़ी बात यह है कि वे रचना योग्य हैं। इसलिए वे पहली बार में थोड़े बोझिल होते हैं, लेकिन वे जितना अधिक आप उनका उपयोग करते हैं, उतना जमीन हासिल करते रहते हैं। इसके अलावा, वे परीक्षण क्षमता के लिए महान हैं, क्योंकि आपको केवल व्यक्तिगत लेंस का परीक्षण करने की आवश्यकता है, और उनकी संरचना के लिए अनुमति दे सकते हैं।

इसलिए, इस उत्तर के अंत में दिए गए कार्यान्वयन के आधार पर, यहां बताया गया है कि आप इसे लेंस के साथ कैसे करेंगे। सबसे पहले, एक पते में एक ज़िप कोड और एक व्यक्ति में एक पता बदलने के लिए लेंस घोषित करें:

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

अंत में, राज बदलने के लिए उस लेंस का उपयोग करें:

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)

इस उदाहरण के लिए उपयोग किया गया सरल कार्यान्वयन, स्कैलाज़ से लिया गया है:

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 ज़रूर संपर्क? मुझे ऐसे प्लगइन के बारे में पता नहीं था।
डैनियल सी। सोबरल

1
कोड personZipCodeLens.set(raj, personZipCodeLens.get(raj) + 1)समान हैpersonZipCodeLens mod (raj, _ + 1)
रॉन

@ron modलेंस के लिए एक आदिम नहीं है, हालांकि।
डैनियल सी। सोबरल

टोनी मॉरिस ने इस विषय पर एक महान पत्र लिखा है । मुझे लगता है कि आपको इसे अपने उत्तर में जोड़ना चाहिए।
लापताफैक्टर

11

लेंस का उपयोग करने के लिए उपयोगी उपकरण:

बस जोड़ना चाहते हैं कि स्केल 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))

मैक्रोस्कोम का उपयोग करना:

यह वर्तमान संकलित रन में परिभाषित केस कक्षाओं के लिए भी काम करता है।

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

आप शायद रिलिट से चूक गए जो और भी बेहतर है। :-) github.com/akisaarinen/rillit
गुमशुदा

अच्छा है, कि जाँच करेगा
सेबस्टियन लम्बर

1
Btw मैंने अपना उत्तर Rillit को शामिल करने के लिए संपादित किया, लेकिन मुझे वास्तव में समझ में नहीं आया कि Rillit बेहतर क्यों है, वे पहली नज़र में एक ही क्रियाशीलता में एक ही कार्यक्षमता प्रदान करते प्रतीत होते हैं @missingfaktor
सेबस्टियन

@SebastienLorber मजेदार तथ्य: Rillit फिनिश है और इसका मतलब लेंस :)
काई सेलग्रेन है

Macrocosm और Rillit दोनों पिछले 4 वर्षों में अपडेट नहीं हुए हैं।
एरिक वैन ओस्टेन

9

मैं सबसे अच्छा वाक्य रचना और सबसे अच्छा कार्यक्षमता और यहां नहीं बताई गई एक पुस्तकालय है कि क्या स्काला पुस्तकालय के लिए चारों ओर देख रहा है है मोनोकल जो मेरे लिए वास्तव में अच्छा रहा है। एक उदाहरण इस प्रकार है:

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

ये बहुत अच्छे हैं और लेंस को संयोजित करने के कई तरीके हैं। उदाहरण के लिए स्कैलाज़ बहुत सारे बॉयलरप्लेट की मांग करता है और यह त्वरित संकलन करता है और बहुत अच्छा चलता है।

अपने प्रोजेक्ट में उनका उपयोग करने के लिए बस इसे अपनी निर्भरता में जोड़ें:

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डैनियल सी। सोबराल के उत्तर में कोड का उपयोग करके समाप्त कर दिया और इसलिए बाहरी निर्भरता जोड़ने से बचा।
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

शायद क्विकलेन आपके सवाल से बेहतर मेल खाता है। क्विकलेन मैक्रो का उपयोग आईडीई फ्रेंडली एक्सप्रेशन को ऐसी चीज़ में बदलने के लिए करता है जो मूल कॉपी स्टेटमेंट के करीब है।

दो उदाहरण मामले वर्गों को देखते हुए:

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 को इसके साथ अपडेट कर सकते हैं:

import com.softwaremill.quicklens._
val updatedRaj = raj.modify(_.address.zipCode).using(_ + 1)
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.