Более чистый способ обновления вложенных структур - PullRequest
121 голосов
/ 10 октября 2010

Скажите, у меня есть следующие два case class es:

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) для обновления таких вложенных структур?

Ответы [ 7 ]

182 голосов
/ 08 апреля 2011

Забавно, что никто не добавил линз, так как они были СДЕЛАНЫ для такого рода вещей.Итак, здесь - это справочный документ по CS, здесь - блог, в котором кратко рассматриваются вопросы использования линз в Scala, здесь - реализация линз для Scalaz и здесь - это некоторый код, использующий его, который удивительно похож на ваш вопрос.И, чтобы сократить котельную, вот плагин, который генерирует линзы 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

Наконец, используйте этот объектив, чтобы изменить 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
}
94 голосов
/ 10 октября 2010

Молния

Молния Huet обеспечивает удобный обход и «мутацию» неизменной структуры данных. Scalaz предоставляет Zippers для 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))

Обратите внимание, что Kiama выходит за пределы системы типов для достижения этой цели.

11 голосов
/ 03 мая 2013

Полезные инструменты для использования линз:

Просто хочу добавить, что проекты 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))

Использование макрокосма:

Это работает даже для классов дел, определенных в текущем прогоне компиляции.

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
9 голосов
/ 21 июня 2014

Я искал, какая библиотека 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 голосов
/ 20 ноября 2014

Бесформенный трюк:

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

Обратите внимание, что в то время как некоторые другие ответы здесь позволяют вам составлять линзы, чтобы углубиться в заданную структуру, эти бесформенные линзы (и другие библиотеки / макросы) позволяют вам комбинировать две несвязанные линзы, так что вы можете создать линзу, которая устанавливает произвольное количество параметров в произвольные позиции в вашей структуре. Для сложных структур данных такая дополнительная композиция очень полезна.

7 голосов
/ 16 сентября 2014

Благодаря своей составной природе, линзы обеспечивают очень хорошее решение проблемы плотно вложенных структур. Однако из-за низкого уровня вложенности я иногда чувствую, что линзы - это слишком много, и я не хочу вводить весь подход линз, если есть только несколько мест с вложенными обновлениями. Для полноты, вот очень простое / прагматичное решение для этого случая:

Я просто пишу несколько 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 голосов
/ 20 апреля 2017

Возможно 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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...