Idiomatic way to update value in a Map based on previous value
There's no adjust
in the Map
API, unfortunately. I've sometimes used a function like the following (modeled on Haskell's Data.Map.adjust
, with a different order of arguments):
def adjust[A, B](m: Map[A, B], k: A)(f: B => B) = m.updated(k, f(m(k)))
Now adjust(m, "Mark")(_ - 50)
does what you want. You could also use the pimp-my-library pattern to get the more natural m.adjust("Mark")(_ - 50)
syntax, if you really wanted something cleaner.
(Note that the short version above throws an exception if k
isn't in the map, which is different from the Haskell behavior and probably something you'd want to fix in real code.)
This could be done with lenses. The very idea of a lens is to be able to zoom in on a particular part of an immutable structure, and be able to 1) retrieve the smaller part from a larger structure, or 2) create a new larger structure with a modified smaller part. In this case, what you desire is #2.
Firstly, a simple implementation of Lens
, stolen from this answer, stolen from 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
}
Next, a smart constructor to create a lens from "larger structure" Map[A,B]
to "smaller part" Option[B]
. We indicate which "smaller part" we want to look at by providing a particular key. (Inspired by what I remember from Edward Kmett's presentation on Lenses in 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)
}
)
Now your code can be written:
val m2 = containsKey("Mark").mod(m)(_.map(_ - 50))
n.b. I actually changed mod
from the answer I stole it from so that it takes its inputs curried. This helps to avoid extra type annotations. Also notice _.map
, because remember, our lens is from Map[A,B]
to Option[B]
. This means the map will be unchanged if it does not contain the key "Mark"
. Otherwise, this solution ends up being very similar to the adjust
solution presented by Travis.
Starting Scala 2.13
, Map#updatedWith
serves this exact purpose:
// 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)
or in a more compact form:
map.updatedWith("Mark")(_.map(_ - 50))
Note that (quoting the doc) if the remapping function returns Some(v)
, the mapping is updated with the new value v
. If the remapping function returns None
, the mapping is removed (or remains absent if initially absent).
def updatedWith[V1 >: V](key: K)(remappingFunction: (Option[V]) => Option[V1]): Map[K, V1]
This way, we can elegantly handle cases where the key for which to update the value doesn't exist:
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)
An SO Answer proposes another alternative, using the |+|
operator from scalaz
val m2 = m |+| Map("Mark" -> -50)
The |+|
operator will sum the values of an existing key, or insert the value under a new key.