Идиоматический способ обновить значение в карте на основе предыдущего значения - PullRequest
41 голосов
/ 25 января 2012

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

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

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

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

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

Ответы [ 4 ]

34 голосов
/ 25 января 2012

В API Map, к сожалению, нет adjust. Иногда я использовал функцию, подобную следующей (смоделированную на 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 и, вероятно, что-то, что вы бы хотели исправить в реальном коде.)

12 голосов
/ 26 января 2012

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

Во-первых, простая реализация Lens, украденная у этот ответ , украденная из скаляза:

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, представленное Travis.

9 голосов
/ 26 февраля 2014

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

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

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

0 голосов
/ 28 марта 2019

Начиная с Scala 2.13, 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))

Обратите внимание, что (цитируя doc ), если функция переназначения возвращает Some(v), отображение обновляется новым значением v.Если функция переназначения возвращает None, отображение удаляется (или остается отсутствующим, если изначально отсутствует).

def updatedWith [V1>: V] (ключ: K) (remappingFunction: (Option [V]) => Option [V1]): Map [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)
...