Why do .map.flatten and flatMap on a Scala Map return different results?

Viewed 1060

I heard many people say flatMap is similar to map + flatten. For example, the answer.

Quite a difference, right?
Because flatMap treats a String as a sequence of Char, it flattens the resulting list of strings into a sequence of characters (Seq[Char]).
flatMap is a combination of map and flatten, so it first runs map on the sequence, then runs flatten giving the result shown.

But I have some code problems today. The result of map and flatMap seems to be different. Here are my code

object ListDemo {
  def main(args: Array[String]): Unit = {
    val map1 = Map("a" -> List(1 ->11,1->111), "b" -> List(2 -> 22, 2 ->222)).map(_._2).flatten
    val map2 = Map("a" -> List(1 ->11,1->111), "b" -> List(2 -> 22, 2 ->222)).flatMap(_._2)
    map1.foreach(println)
    println()
    map2.foreach(println)
  }
}

the result is not expected.

(1,11)
(1,111)
(2,22)
(2,222)

(1,111)
(2,222)

Why it happened?

3 Answers

When calling .map(f) on a Map with an f that returns (K, V) for some K and V (not necessarily the same K and V types of the original Map), the result will be a Map[K, V]. Otherwise, if f returns some other (non-pair) type T, the result will be an Iterable[T]. So it will be a Map if the function returns a pair and an Iterable otherwise.

In your case the function returns List[(Int, Int)], so the result is an Iterable[List[(Int, Int)]] - not a Map. The .flatten then turns this into an Iterable[(Int, Int)].

When using flatMap directly, you directly end up with the (Int, Int) pairs, so the result will be a Map[Int, Int] - not an Iterable[(Int, Int)]. And since Maps don't allow duplicate keys, the Map contains less elements than the Iterable.

You are kind of misunderstanding things here,

So, Lets say you have x: M[A] and f: A = N[B]for any Monad M and N then x.flatMap(f) should be same as x.map(f).flatten.

But what you have here is kind of a nested monad map: M[N[A]] and your function is f: A => B with following aliasing,

scala> type MapWithStringKey[A] = Map[String, A]
// defined type alias MapWithStringKey

scala> type TupleOfInt = (Int, Int)
// defined type alias TupleOfInt

scala> val map: MapWithStringKey[List[TupleOfInt]] = Map("a" -> List(1 ->11,1->111), "b" -> List(2 -> 22, 2 ->222))
// map: MapWithStringKey[List[TupleOfInt]] = Map(a -> List((1,11), (1,111)), b -> List((2,22), (2,222)))

This case is entirely different from above mentioned standard definition which connects flatMap to map and flatten.

Now, It is just one of the non-standards cases where you can choose to use either of the two choices depending on what you want. And when we add the special key-uniqueness properties of Map (which is already discussed in answer by @sepp2k), things become even more unpredictable.

TL;DR: the result type of both calls is different. The call to .map().flatten returns a Iterable[(Int, Int)] and the call to .flatMap() returns a Map[Int, Int]. Since a map may not contain the same key twice, the first entry per key is overwritten by the second entry.

Consider a Map as a Iterable[(Key,Value)]. When calling .map, you will have to provide it a function that returns a tuple, (Key, Value) (the actual types may be different from the original Key and Value).

In your example, Value happens to be a List[(Int, Int)]. When calling .map, and returning the Value of the original Map, you end up with a Iterable[List[(Int, Int)]], which your call to .flatten turns into a Iterable[(Int, Int)] by concatenating the 'inner' lists together. If you were to turn that into a map (by calling .toMap), you would see the same result as with flatMap.

Now, flatMap is different in that it expects a return type of Seq[(Key, Value)], rather than just (Key, Value). It then uses the returned value as the entries in a newly constructed Map.

In your case, your original Value of List[(Int, Int)] satisfies the expected return type, converting you original Map[(String, List[(Int, Int)] into a Map[(Int, Int)]. Since a map cannot contain two entries with the same keys, the second occurrence of the key replaces the earlier occurrence.

To see this behavior, it helps to use the REPL (just run scala) instead of writing a main class, so you can see the intermediate values and their types.

scala> val theMap = Map("a" -> List(1 ->11,1->111), "b" -> List(2 -> 22, 2 ->222))
theMap: scala.collection.immutable.Map[String,List[(Int, Int)]] = Map(a -> List((1,11), (1,111)), b -> List((2,22), (2,222)))

scala> val mapped = theMap.map(_._2)
mapped: scala.collection.immutable.Iterable[List[(Int, Int)]] = List(List((1,11), (1,111)), List((2,22), (2,222)))

scala> val flattened = mapped.flatten
flattened: scala.collection.immutable.Iterable[(Int, Int)] = List((1,11), (1,111), (2,22), (2,222))

scala> val flatMapped = theMap.flatMap(_._2)
flatMapped: scala.collection.immutable.Map[Int,Int] = Map(1 -> 111, 2 -> 222)

scala> val flattenedToMap = flattened.toMap
flattenedToMap: scala.collection.immutable.Map[Int,Int] = Map(1 -> 111, 2 -> 222)
Related