Idiomatic way to update value in a Map based on previous value
Asked Answered
J

4

54

Let's say I store bank accounts information in an immutable Map:

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

and I want to withdraw, say, $50 from Mark's account. I can do it as follows:

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

But this code seems ugly to me. Is there better way to write this?

Jennine answered 25/1, 2012 at 13:57 Comment(0)
F
41

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.)

Fruin answered 25/1, 2012 at 14:23 Comment(3)
This only works if the key exists in the map. Consider map.get(k).fold(map)(b => map.updated(k, f(b))) if you want to ignore a missing key, or an approach that has f: Option[B] => B if you want to be able to set the key in its absence.Levenson
@adamnfish: adjust(m, "Mark")(_.getOrElse(0) - 50)Adenovirus
Very useful - I wish adjust were in the standard library! (probably @adamnfish's fold variant..)Schleicher
M
15

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)
Manis answered 28/3, 2019 at 19:30 Comment(1)
This seems like the most correct solution in the given scala 2.13+Pointer
H
12

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.

Haag answered 25/1, 2012 at 20:18 Comment(0)
I
10

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.

Intumescence answered 26/2, 2014 at 16:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.