VSh
VSh

Reputation: 478

Why for-loop with yield accumulates into a Map instead of a List?

I have the following code:

val dummy = Map(1 -> Map(2 -> 3.0,
                         4 -> 5.0),
                6 -> Map(7 -> 8.0))

val thisIsList = for (x <- dummy; y <- x._2.keys) yield s"(${x._1}, ${y})"
println(thisIsList)  // List((1, 2), (1, 4), (6, 7))

val thisIsMap = for (x <- dummy; y <- x._2.keys) yield new Tuple2(x._1, y)
println(thisIsMap)   // Map(1 -> 4, 6 -> 7) - this is not what I want    

I would expect the second statement to produce a list of tuples, but instead it returns a Map. I found an explanation here scala: yield a sequence of tuples instead of map on why Map is returned, but I'm still struggling to find an elegant way to return a list of tuples instead in this case.

Upvotes: 1

Views: 541

Answers (3)

Aluan Haddad
Aluan Haddad

Reputation: 31823

This is because of how the for comprehension syntax is transformed by the compiler into a series of method calls. map, flatMap, and withFilter are targeted by the permutations of for comprehensions. This is very powerful and general because it allows the syntax to work with arbitrary types. There's more to this, such as the CanBuildFrom implicit, but essentially mapping a Map to an Iterable[Tuple[A, B]] produces a Map[A, B]. The signature is actually overloaded for Map to provide this behavior

Specifically, given your original code below

val thisIsMap = for (x <- dummy; y <- x._2.keys) yield new Tuple2(x._1, y)
println(thisIsMap)   // Map(1 -> 4, 6 -> 7) - this is not what I want

The translation looks roughly like this

val thisIsMap = dummy.flatMap { x =>
  x._2.keys.map { y =>
    (x._1, y)
  }
}

See this fiddle

In order to obtain a list as desired, we can write

val thisIsMap = (for (x <- dummy; y <- x._2.keys) yield (x._1, y)).toList

However, if we consider what we've learned about for comprehensions, we can write it more elegantly as

val thisIsMap = for (x <- dummy.toList; y <- x._2.keys) yield (x._1, y)

In the above, we have leveraged the very behavior that confounded the original code by inferring that a for comprehension over a List will produce a List.

However, note the difference between converting the source into a List as opposed to converting the resulting map into a List after the comprehension.

If we call toList on the source (dummy) we get List((1,2), (1,4), (6,7)) while if we call it on the result, we get List((1,4), (6,7)), for self evident reasons so choose carefully and deliberately.

Upvotes: 7

VSh
VSh

Reputation: 478

After working through the answers, will post a TLDR summary to my own question here.

The type of the data structure which is returned back by the for comprehension loop is expected to be the same, as the type which the for loop starts iterating over. I.e. if it starts iterating over Map - expect Map to be the end result.

val thisIsList = for (x <- dummy; y <- x._2.keys) yield s"(${x._1}, ${y})"
println(thisIsList)  // List((1, 2), (1, 4), (6, 7))

In this example from the question, the for loop starts iterating over Map, but returns a List. Which is happening because the yield doesn't return the type which can be converted to a Map. But it can be converted to a List, so Scala does it.

val thisIsMap = for (x <- dummy; y <- x._2.keys) yield new Tuple2(x._1, y)
println(thisIsMap)   // Map(1 -> 4, 6 -> 7) - this is not what I want 

In this example though, everything is happening as it should, but because the resulting Map cannot have duplicate keys, the tuple (1,2) gets overwritten by the tuple (1,4). I.e. the map contains only 2 elements.

Upvotes: 0

Mario Galic
Mario Galic

Reputation: 48420

Try

dummy
  .view
  .mapValues(_.keys.toList)
  .flatMap { case (key: Int, values: List[Int]) => values.map((key, _)) }
  .toList

which outputs

res0: List[(Int, Int)] = List((1,2), (1,4), (6,7)

Upvotes: 2

Related Questions