nomad
nomad

Reputation: 1809

Nested Default Maps in Scala

I'm trying to construct nested maps in Scala, where both the outer and inner map use the "withDefaultValue" method. For example, the following :

val m = HashMap.empty[Int, collection.mutable.Map[Int,Int]].withDefaultValue( HashMap.empty[Int,Int].withDefaultValue(3))
m(1)(2)
res: Int = 3
m(1)(2) = 5
m(1)(2) 
res: Int = 5
m(2)(3) = 6
m
res : scala.collection.mutable.Map[Int,scala.collection.mutable.Map[Int,Int]] = Map()

So the map, when addressed by the appropriate keys, gives me back what I put in. However, the map itself appears empty! Even m.size returns 0 in this example. Can anyone explain what's going on here?

Upvotes: 10

Views: 2962

Answers (5)

Alberto
Alberto

Reputation: 607

I know it's a bit late but I've just seen the post while I was trying to solve the same problem.
Probably the API are different from the 2012 version but you may want to use withDefaultinstead that withDefaultValue.
The difference is that withDefault takes a function as parameter, that is executed every time a missed key is requested ;)

Upvotes: 0

Ole
Ole

Reputation: 533

I just had the exact same problem, and was happy to find dhg's answer. Since typing getOrElseUpdate all the time is not very concise, I came up with this little extension of the idea that I want to share: You can declare a class that uses getOrElseUpdate as default behavior for the () operator:

class DefaultDict[K, V](defaultFunction: (K) => V) extends HashMap[K, V] {
  override def default(key: K): V = return defaultFunction(key)
  override def apply(key: K): V = 
    getOrElseUpdate(key, default(key))
}

Now you can do what you want to do like this:

var map = new DefaultDict[Int, DefaultDict[Int, Int]](
  key => new DefaultDict(key => 3))
map(1)(2) = 5

Which does now result in map containing 5 (or rather: containing a DefaultDict containing the value 5 for the key 2).

Upvotes: 1

dhg
dhg

Reputation: 52681

Short answer

It's definitely not a bug.

Long answer

The behavior of withDefaultValue is to store a default value (in your case, a mutable map) inside the Map to be returned in the case that they key does not exist. This is not the same as a value that is inserted into the Map when they key is not found.

Let's look closely at what's happening. It will be easier to understand if we pull the default map out as a separate variable so we can inspect is at will; let's call it default

import collection.mutable.HashMap
val default = HashMap.empty[Int,Int].withDefaultValue(3)

So default is a mutable map (that has its own default value). Now we can create m and give default as the default value.

import collection.mutable.{Map => MMap}
val m = HashMap.empty[Int, MMap[Int,Int]].withDefaultValue(default)

Now whenever m is accessed with a missing key, it will return default. Notice that this is the exact same behavior as you have because withDefaultValue is defined as:

def withDefaultValue (d: B): Map[A, B]

Notice that it's d: B and not d: => B, so it will not create a new map each time the default is accessed; it will return the same exact object, what we've called default.

So let's see what happens:

m(1) // Map()

Since key 1 is not in m, the default, default is returned. default at this time is an empty Map.

m(1)(2) = 5

Since m(1) returns default, this operation stores 5 as the value for key 2 in default. Nothing is written to the Map m because m(1) resolves to default which is a separate Map entirely. We can check this by viewing default:

default // Map(2 -> 5)

But as we said, m is left unchanged

m // Map()

Now, how to achieve what you really wanted? Instead of using withDefaultValue, you want to make use of getOrElseUpdate:

def getOrElseUpdate (key: A, op: ⇒ B): B

Notice how we see op: => B? This means that the argument op will be re-evaluated each time it is needed. This allows us to put a new Map in there and have it be a separate new Map for each invalid key. Let's take a look:

val m2 = HashMap.empty[Int, MMap[Int,Int]]

No default values needed here.

m2.getOrElseUpdate(1, HashMap.empty[Int,Int].withDefaultValue(3)) // Map()

Key 1 doesn't exist, so we insert a new HashMap, and return that new value. We can check that it was inserted as we expected. Notice that 1 maps to the newly added empty map and that they 3 was not added anywhere because of the behavior explained above.

m2 // Map(1 -> Map())

Likewise, we can update the Map as expected:

m2.getOrElseUpdate(1, HashMap.empty[Int,Int].withDefaultValue(1))(2) = 6

and check that it was added:

m2 // Map(1 -> Map(2 -> 6))

Upvotes: 16

leedm777
leedm777

Reputation: 24032

What you're seeing is the effect that you've created a single Map[Int, Int] this is the default value whenever the key isn't in the outer map.

scala> val m = HashMap.empty[Int, collection.mutable.Map[Int,Int]].withDefaultValue( HashMap.empty[Int,Int].withDefaultValue(3))
m: scala.collection.mutable.Map[Int,scala.collection.mutable.Map[Int,Int]] = Map()

scala> m(2)(2)
res1: Int = 3

scala> m(1)(2) = 5

scala> m(2)(2)
res2: Int = 5

To get the effect that you're looking for, you'll have to wrap the Map with an implementation that actually inserts the default value when a key isn't found in the Map.

Edit:

I'm not sure what your actual use case is, but you may have an easier time using a pair for the key to a single Map.

scala> val m = HashMap.empty[(Int, Int), Int].withDefaultValue(3)
m: scala.collection.mutable.Map[(Int, Int),Int] = Map()

scala> m((1, 2))
res0: Int = 3

scala> m((1, 2)) = 5

scala> m((1, 2))
res3: Int = 5

scala> m
res4: scala.collection.mutable.Map[(Int, Int),Int] = Map((1,2) -> 5)

Upvotes: 0

huynhjl
huynhjl

Reputation: 41646

withDefaultValue is used to return a value when the key was not found. It does not populate the map. So you map stays empty. Somewhat like using getOrElse(a, b) where b is provided by withDefaultValue.

Upvotes: 2

Related Questions