bluenote10
bluenote10

Reputation: 26550

How to access/initialize and update values in a mutable map?

Consider the simple problem of using a mutable map to keep track of occurrences/counts, i.e. with:

val counts = collection.mutable.Map[SomeKeyType, Int]()

My current approach to incrementing a count is:

counts(key) = counts.getOrElse(key, 0) + 1
// or equivalently
counts.update(key, counts.getOrElse(key, 0) + 1)

This somehow feels a bit clumsy, because I have to specify the key twice. In terms of performance, I would also expect that key has to be located twice in the map, which I would like to avoid. Interestingly, this access and update problem would not occur if Int would provide some mechanism to modify itself. Changing from Int to a Counter class that provides an increment function would for instance allow:

// not possible with Int
counts.getOrElseUpdate(key, 0) += 1
// but with a modifiable counter
counts.getOrElseUpdate(key, new Counter).increment

Somehow I'm always expecting to have the following functionality with a mutable map (somewhat similar to transform but without returning a new collection and on a specific key with a default value):

// fictitious use
counts.updateOrElse(key, 0, _ + 1)
// or alternatively
counts.getOrElseUpdate(key, 0).modify(_ + 1)

However as far as I can see, such a functionality does not exist. Wouldn't it make sense in general (performance and syntax wise) to have such a f: A => A in-place modification possibility? Probably I'm just missing something here... I guess there must be some better solution to this problem making such a functionality unnecessary?

Update:

I should have clarified that I'm aware of withDefaultValue but the problem remains the same: performing two lookups is still twice as slow than one, no matter if it is a O(1) operation or not. Frankly, in many situations I would be more than happy to achieve a speed-up of factor 2. And obviously the construction of the modification closure can often be moved outside of the loop, so imho this is not a big issue compared to running an operation unnecessarily twice.

Upvotes: 27

Views: 18699

Answers (3)

Xavier Guihot
Xavier Guihot

Reputation: 61666

Starting Scala 2.13, Map#updateWith serves this exact purpose:

map.updateWith("a")({
  case Some(count) => Some(count + 1)
  case None        => Some(1)
})

def updateWith(key: K)(remappingFunction: (Option[V]) => Option[V]): Option[V]


For instance, if the key doesn't exist:

val map = collection.mutable.Map[String, Int]()
// map: collection.mutable.Map[String, Int] = HashMap()

map.updateWith("a")({ case Some(count) => Some(count + 1) case None => Some(1) })
// Option[Int] = Some(1)
map
// collection.mutable.Map[String, Int] = HashMap("a" -> 1)

and if the key exists:

map.updateWith("a")({ case Some(count) => Some(count + 1) case None => Some(1) })
// Option[Int] = Some(2)
map
// collection.mutable.Map[String, Int] = HashMap("a" -> 2)

Upvotes: 6

Peter L
Peter L

Reputation: 3343

I wanted to lazy-initialise my mutable map instead of doing a fold (for memory efficiency). The collection.mutable.Map.getOrElseUpdate() method suited my purposes. My map contained a mutable object for summing values (again, for efficiency).

        val accum = accums.getOrElseUpdate(key, new Accum)
        accum.add(elem.getHours, elem.getCount)

collection.mutable.Map.withDefaultValue() does not keep the default value for a subsequent requested key.

Upvotes: 1

coltfred
coltfred

Reputation: 1480

You could create the map with a default value, which would allow you to do the following:

scala> val m = collection.mutable.Map[String, Int]().withDefaultValue(0)
m: scala.collection.mutable.Map[String,Int] = Map()

scala> m.update("a", m("a") + 1)

scala> m
res6: scala.collection.mutable.Map[String,Int] = Map(a -> 1)

As Impredicative mentioned, map lookups are fast so I wouldn't worry about 2 lookups.

Update:

As Debilski pointed out you can do this even more simply by doing the following:

scala> val m = collection.mutable.Map[String, Int]().withDefaultValue(0)
scala> m("a") += 1
scala> m
 res6: scala.collection.mutable.Map[String,Int] = Map(a -> 1)

Upvotes: 29

Related Questions