Чистіший спосіб оновлення вкладених структур


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 update-in) оновити такі вкладені структури?


1
Я припускаю, що ви хочете зберегти незмінність, інакше просто дотримуйтесь вар перед декларацією адреси осіб.
GClaramunt

8
@GClaramunt: Так, я хочу зберегти незмінність.
зниклий фактор

Відповіді:


94

Блискавки

Застібка-блискавка Huet забезпечує зручне переміщення та "мутацію" незмінної структури даних. Scalaz надає блискавки для Stream( scalaz.Zipper ) та Tree( scalaz.TreeLoc ). Виявляється, структура блискавки автоматично виводиться з оригінальної структури даних таким чином, що нагадує символічну диференціацію алгебраїчного виразу.

Але як це допомагає вам у ваших кейсах Scala? Ну, Лукаш Ріц нещодавно прототипував розширення для 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, що ці зусилля слід продовжувати та інтегрувати у компілятор.

До речі, Лукаше нещодавно опублікував версію Pacman, програмовану користувачем через DSL. Схоже, він не використовував модифікований компілятор, тому що я не бачу @zipанотацій.

Переписування дерева

В інших обставинах ви можете застосувати деяку трансформацію у всій структурі даних відповідно до деякої стратегії (зверху вниз, знизу вгору) та на основі правил, які відповідають значенню в якийсь момент структури. Класичний приклад - це перетворення AST для мови, можливо, для оцінки, спрощення або збору інформації. Kiama підтримує переписування , дивіться приклади в 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))

Зауважте, що Кіама крокує поза типовою системою, щоб досягти цього.


2
Для тих, хто шукає комітет. Ось це: github.com/soundrabbit/scala/commit/… (я думаю ..)
IttayD

15
Гей, де лінзи?
Даніель К. Собрал

Я щойно стикався з цією проблемою, і ідея @zip звучить дуже фантастично, можливо, її слід навіть зайняти так далеко, щоб усі класи класів мали її? Чому це не реалізовано? Лінзи хороші, але для великих та багатьох класів / класів це просто котельня, якщо ви просто хочете сеттер і нічого фантазійного, як інкремент.
Йохан Ш

186

Смішно, що ніхто не додавав лінзи, оскільки вони зроблені для подібних матеріалів. Отже, ось довідковий документ CS на цьому сайті, ось блог, який коротко торкається використання лінз у Scala, ось реалізація лінз для Scalaz, і ось якийсь код, який використовує його, виглядає напрочуд як ваше запитання. І, щоб скоротити на плиті котла, ось плагін, який генерує лінзи Scalaz для класів корпусів.

Щодо бонусних балів, ось ще одне питання, яке стосується об’єктивів, та документ Тоні Морріса.

Велика справа щодо лінз полягає в тому, що вони є композиційними. Тож вони спочатку трохи громіздкі, але вони все більше набирають сили, чим більше ви їх використовуєте. Крім того, вони чудово підходять для перевірки, оскільки вам потрібно лише протестувати окремі лінзи, і ви можете прийняти як належне їх склад.

Отже, виходячи з реалізації, наданої наприкінці цієї відповіді, ось як би ви це зробили за допомогою лінз. Спочатку оголосіть лінзи, щоб змінити поштовий індекс у адресу та адресу особи:

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
Можливо, ви захочете оновити цю відповідь описом плагіна лінзи Герольфа Сейца.
зниклий фактор

@missingfaktor Звичайно. Посилання? Мені не було відомо про такий плагін.
Даніель К. Собрал

1
Код personZipCodeLens.set(raj, personZipCodeLens.get(raj) + 1)такий же, якpersonZipCodeLens mod (raj, _ + 1)
ron

@ron, однак mod, не примітив для лінз.
Даніель К. Собрал

Тоні Морріс написав чудову працю на цю тему. Я думаю, ви повинні зв’язати це у своїй відповіді.
зниклий фактор

11

Корисні інструменти для використання лінз:

Просто хочу додати , що макрокосм і 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))

Використання макрокосму:

Це працює навіть для класів випадків, визначених у поточному запуску компіляції.

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

Приємно, перевіримо це
Себастьян Лорбер

1
До речі, я відредагував свою відповідь, щоб включити Rillit, але я не дуже розумію, чому Rillit краще, вони, здається, забезпечують ту саму функціональність у тому ж багатослівному з першого погляду @missingfaktor
Sebastien Lorber

@SebastienLorber Веселий факт: Rillit - фінська мова і означає лінзи :)
Kai Sellgren

І Macrocosm, і Rillit, здається, не були оновлені протягом останніх 4 років.
Ерік ван Оостен

9

Я роздивлявся, яка бібліотека Scala, яка має найприємніший синтаксис і найкращий функціонал, і одна бібліотека, яку не згадується тут, - це монокль, який для мене був дуже хорошим. Приклад наступний:

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

Shapeless робить трюк:

"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

Можливо, QuickLens краще відповідає вашому питанню. QuickLens використовує макроси для перетворення IDE-дружнього виразу в те, що є близьким до оригінального оператора копіювання.

З урахуванням двох прикладних класів:

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 за допомогою:

import com.softwaremill.quicklens._
val updatedRaj = raj.modify(_.address.zipCode).using(_ + 1)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.