Идиоматический способ обновления значения в Карте на основе предыдущего значения
Скажем, я сохраняю информацию о банковских счетах в неизменяемом Map
:
val m = Map("Mark" -> 100, "Jonathan" -> 350, "Bob" -> 65)
и я хочу снять, скажем, 50 долларов с учетной записи Mark. Я могу сделать это следующим образом:
val m2 = m + ("Mark" -> (m("Mark") - 50))
Но этот код кажется мне уродливым. Есть ли лучший способ написать это?
Ответы
Ответ 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)