Подтвердить что ты не робот

Идиоматический способ обновления значения в Карте на основе предыдущего значения

Скажем, я сохраняю информацию о банковских счетах в неизменяемом Map:

val m = Map("Mark" -> 100, "Jonathan" -> 350, "Bob" -> 65)

и я хочу снять, скажем, 50 долларов с учетной записи Mark. Я могу сделать это следующим образом:

val m2 = m + ("Mark" -> (m("Mark") - 50))

Но этот код кажется мне уродливым. Есть ли лучший способ написать это?

4b9b3361

Ответ 1

Там нет adjust в API Map, к сожалению. Я иногда использовал функцию, подобную следующей (по модели Haskell Data.Map.adjust с другим порядком аргументов):

def adjust[A, B](m: Map[A, B], k: A)(f: B => B) = m.updated(k, f(m(k)))

Теперь adjust(m, "Mark")(_ - 50) делает то, что вы хотите. Вы также можете использовать рисунок pimp-my-library, чтобы получить более естественный синтаксис m.adjust("Mark")(_ - 50), если вы действительно хотели что-то более чистое.

(Обратите внимание, что короткая версия выше выдает исключение, если k не находится на карте, что отличается от поведения Haskell и, вероятно, что-то, что вы хотите исправить в реальном коде.)

Ответ 2

Это можно сделать с помощью линз. Сама идея объектива заключается в том, чтобы иметь возможность приближать к определенной части неизменяемой структуры и иметь возможность: 1) извлечь меньшую часть из более крупной структуры или 2) создать новую большую структуру с модифицированной меньшей частью, В этом случае вам нужно 2.

Во-первых, простая реализация Lens, украденная этим ответом, украденная из 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
}

Далее, умный конструктор для создания объектива с "большей структурой" Map[A,B] до "меньшей части" Option[B]. Мы указываем, какую "меньшую часть" мы хотим рассмотреть, предоставив конкретный ключ. (Вдохновленный тем, что я помню из Презентация Эдварда Кеммета на объективах в Scala):

def containsKey[A,B](k: A) = Lens[Map[A,B], Option[B]](
  get = (m:Map[A,B]) => m.get(k),
  set = (m:Map[A,B], opt: Option[B]) => opt match {
    case None => m - k
    case Some(v) => m + (k -> v)
  }
)

Теперь ваш код можно записать:

val m2 = containsKey("Mark").mod(m)(_.map(_ - 50))

n.b. Я фактически изменил mod ответ, который я украл, так что он берет свои входы в карри. Это помогает избежать аннотаций дополнительного типа. Также обратите внимание на _.map, потому что помните, что наш объектив от Map[A,B] до Option[B]. Это означает, что карта не изменится, если она не содержит ключ "Mark". В противном случае это решение оказывается очень похожим на решение adjust, представленное Трэвисом.

Ответ 3

SO Answer предлагает другую альтернативу, используя оператор |+| из scalaz

val m2 = m |+| Map("Mark" -> -50)

Оператор |+| суммирует значения существующего ключа или вставляет значение под новым ключом.

Ответ 4

Начиная с :V](key:K)(remappingFunction:Option[V]=>Option[V1]):CC[K,V1] rel="nofollow noreferrer"> Map#updatedWith Scala 2.13, :V](key:K)(remappingFunction:Option[V]=>Option[V1]):CC[K,V1] rel="nofollow noreferrer"> Map#updatedWith этой целью:

// val map = Map("Mark" -> 100, "Jonathan" -> 350, "Bob" -> 65)
map.updatedWith("Mark") {
  case Some(money) => Some(money - 50)
  case None        => None
}
// Map("Mark" -> 50, "Jonathan" -> 350, "Bob" -> 65)

или в более компактной форме:

map.updatedWith("Mark")(_.map(_ - 50))

Обратите внимание, что (цитируя :V](key:K)(remappingFunction:Option[V]=>Option[V1]):CC[K,V1] rel="nofollow noreferrer">документ), если функция переназначения возвращает Some(v), отображение обновляется новым значением v. Если функция переназначения возвращает None, отображение удаляется (или остается отсутствующим, если изначально отсутствует).

def updatedWith [V1>: V] (ключ: K) (remappingFunction: (Option [V]) => Option [V1]): Карта [K, V1]

Таким образом, мы можем элегантно обрабатывать случаи, когда ключ для обновления значения не существует:

Map("Jonathan" -> 350, "Bob" -> 65)
  .updatedWith("Mark")({ case None => Some(0) case Some(v) => Some(v - 50) })
// Map("Jonathan" -> 350, "Bob" -> 65, "Mark" -> 0)
Map("Mark" -> 100, "Jonathan" -> 350, "Bob" -> 65)
  .updatedWith("Mark")({ case None => Some(0) case Some(v) => Some(v - 50) })
// Map("Mark" -> 50, "Jonathan" -> 350, "Bob" -> 65)

Map("Jonathan" -> 350, "Bob" -> 65)
  .updatedWith("Mark")({ case None => None case Some(v) => Some(v - 50) })
// Map("Jonathan" -> 350, "Bob" -> 65)