How to retrieve the value of a nested map using the value of the first map?

Viewed 191

I have a map of entries of type Map[String, String] and a lookup map of type Map[String, Map[String, String]]. My goal is to look in the first map for a match in the second map, once I have a key match I have to replace the value in the first dictionary with the look up of the second dictionary. The example will clear things up.

I've come up with an algorithm so far but I am getting a strange answer of Some(e) and Some(t).

The first entry map is the following:

val entries = Map("fruit" -> "aaa", "animal" -> "bbb", "person" -> "jjj")

The second map is the following:

val lookup = Map("fruit" -> Map("ccc" -> "orange", "aaa" -> "apple"),
                 "animal" -> Map("zzz" -> "dog", "bbb" -> "cat"))

The result I am expecting is the following (Note: "person" was correctly not included in the result as there is no match for it):

val result = Map("fruit" -> "apple", "animal" -> "cat")

The algorithm I came up with is the following:

val res = for{ (k, ev) <- entries 
      lv <- lookup.get(k).get(ev)} yield (k, lv)

this algorithm gives me the following result and I have no idea why:

Map(fruit -> e, animal -> t)

Where is the e and t coming from?

5 Answers

Break it down into its constituent parts.

for {
  (k,v1) <- entries
  submap <- lookup.get(k)
  v2     <- submap.get(v1)
} yield (k,v2)
//res0: immutable.Map[String,String] = Map(fruit -> apple, animal -> cat)

Not sure where the e and t are coming from in your error output.


OK, I figured it out. lv <- lookup.get(k).get(ev) is iterating through every letter of "apple" and "cat" respectively, but as there can be only one key->value pair for each key in a Map, only the final letter is retained.

This will work:

val res = for{ 
    (k, ev) <- entries
    l1 <- lookup.get(k)
    l2 <- l1.get(ev)
} yield { 
    (k, lv)
}

Problem Explanation:

lookup.get(k).get(ev) //this returns a String <- reason: not known

x <- "apple" //x is now a List[Char] -> List('a','p','p','l','e')

Now:

map += ("animal" -> 'a')
map += ("animal" -> 'p')
map += ("animal" -> 'p')
map += ("animal" -> 'l')
map += ("animal" -> 'e')
map += ("fruit" -> 'c')
map += ("fruit" -> 'a')
map += ("fruit" -> 't')

Will result in:

Map("animal" -> 'e', "fruit", 't')

The documentation for yield explains why this happens: https://docs.scala-lang.org/tutorials/FAQ/yield.html

The page, in Example 2, states:

for(x <- c1; y <- c2; z <- c3) yield {...}
is translated into
c1.flatMap(x => c2.flatMap(y => c3.map(z => {...})))

Therefore your comprehension translates to:

entries.flatMap({case (k, ev) => lookup.get(k).get(ev).map(lv => (k, lv)) })

The key here is that each of "apple" and "cat" are strings which map is being done on (i.e. you are iterating over each character of those words). Since a Map is being updated with ("fruit" -> "a") ... ("fruit" -> "e"), only (fruit -> e) is apparent and the final result is what you saw:

Map(fruit -> e, animal -> t)

Some ways to get the intended result:

for{ (k, ev) <- entries ;lv <- lookup.get(k)} yield (k, lv.get(ev).get)

Or avoid chained comprehensions altogether, and just use a regular map:

entries.map({case (k, v) => (k, lookup.get(k).get(v))})

Both of these result in Map(fruit -> apple, animal -> cat) but note that these solutions don't handle the case where get returns a None (i.e. the lookup map is missing that key)

You can also try the following approach for getting the output

val output = entries.map{
  case (k,v) =>
    k -> lookup.get(k).flatMap(_.get(v)).getOrElse(v)
}

This will fetch you the output as

output: scala.collection.immutable.Map[String,String] = Map(fruit -> apple, animal -> cat)

Inline Solution :

val res =  entries.flatMap(e => lookup.get(e._1).flatMap(_.get(e._2).map(e._1->_)))

Output :

res: scala.collection.immutable.Map[String,String] = Map(fruit -> apple, animal -> cat)
Related