Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Update a mutable map with default value in Scala

Tags:

map

scala

Consider the following code that counts the frequency of each string in the list and stores the results in the mutable map. This works great, but I don't understand where the += method is defined?! Is this some weird implicit conversion thing or what? I saw this code somewhere but it didn't include an explanation for the +=.

val list = List("a", "b", "a")
val counts = new scala.collection.mutable.HashMap[String, Int]().withDefaultValue(0)
list.foreach(counts(_) += 1)
counts
//> res7: scala.collection.mutable.Map[String,Int] = Map(a -> 2, b -> 1)

The apply of map returns an Int, but Int doesn't have a += and this method updates the map with a new value, so it looks as if the apply returns a mutable integer that has a += method...

like image 280
frank.durden Avatar asked Jan 30 '13 13:01

frank.durden


2 Answers

This is not an implicit conversion - it is a desugaring. Writing:

x += 1

desugars to:

x = x + 1

if the class of x does not have a += method defined on it.

In the same way:

counts("a") += 1

desugars to:

counts("a") = counts("a") + 1

because counts("a") is an Int, and Int does not have a += method defined.

On the other hand, writing:

x(expression1) = expression2

desugars to a call to the update method in Scala:

x.update(expression1, expression2)

Every mutable Map has an update method defined - it allows setting keys in the map.

So the entire expression is desugared to:

list.foreach(x => counts.update(x, counts(x) + 1))

This += is not to be confused with the += method on mutable.Maps in Scala. That method updates the entry in the map if that key already existed, or adds a new key-value pair. It returns the this reference, that is, the same map, so you can chain += calls. See ScalaDoc or the source code.

like image 159
axel22 Avatar answered Sep 19 '22 20:09

axel22


For these moments where you wonder what compiler magic is happening in a part of your code, scalac -print is your best friend (see this question).

If you do a scalac -print C.scala where C.scala is

package test

class C {
    def myMethod() {
        val counts = new scala.collection.mutable.HashMap[String, Int]().withDefaultValue(0)
        counts("a") += 1
    }
}

you get

package test {
  class C extends Object {
    def myMethod(): Unit = {
      val counts: collection.mutable.Map = new collection.mutable.HashMap().withDefaultValue(scala.Int.box(0));
      counts.update("a", scala.Int.box(scala.Int.unbox(counts.apply("a")).+(1)))
    };
    def <init>(): test.C = {
      C.super.<init>();
      ()
    }
  }

It came as a surprise for me also, but apparently scalac will transform

map(key) =<op> rhs

to

map.update(key, map.apply(key) <op> rhs)
like image 29
Marius Danila Avatar answered Sep 17 '22 20:09

Marius Danila