2

This is my code

def testMap() = {
    val x = Map(
      1 -> Map(
        2 -> 3,
        3 -> 4
      ),
      5 -> Map(
        6 -> 7,
        7 -> 8
      )
    )

    for {
      (a, v) <- x
      (b, c) <- v
    } yield {
      a
    }
  }

The code above gives

List(1, 1, 5, 5)

If I change the yield value of the for comprehension a to (a, b), the result is

Map(1 -> 3, 5 -> 7)

If I change (a, b) to (a, b, c), the result is

List((1,2,3), (1,3,4), (5,6,7), (5,7,8))

My question is what is the mechanism behind the determination of the result type in this for comprehension?

nam
  • 3,542
  • 9
  • 46
  • 68

2 Answers2

2

When you look into the API Documentation into the details of the map-Method you will find, that it has a second, implicit parameter of type CanBuildFrom. An instance of CanBuildFrom from defines how a certain collection is build when mapping over some other collection and a certain element type is provided.

In the case where you get a Map as result, you are mapping over a Map and are providing binary tuples. So the compiler searches for a CanBuildFrom-instance, that can handle that.

To find such an instance, the compiler looks in different places, e.g. the current scope, the class a method is invoked on and its companion object. In this case it will find an implicit field called canBuildFrom in the companion object of Map that is suitable and can be used to build a Map as result. So it tries to infer the result type to Map and as this succeeds uses this instance.

In the case, where you provide single values or triples instead, the instance found in the companion of Map does not have the required type, so it continues searching up the inheritance tree. It finds it in the companion object of Iterable. The instance their allows to build an Iterable of an arbitrary element type. So the compiler uses that.

So why do you get a List? Because that happens to be the implementation used there, the type system only guarantees you an Iterable.

If you want to get an Iterable instead of a Map you can provide a CanBuildFrom instance explicitly (only if you call map and flatMap directly) or just force the return type. There you will also notice that you won't be able to request a List even though you get one.

This wont work:

val l: List[Int] = Map(1->2).map(x=>3)

This however will:

val l: Iterable[Int] = Map(1->2).map(x=>3)
dth
  • 2,287
  • 10
  • 17
0

To add to @dth, if you want a list, you can do:

val l = Map(1->2,3->4).view.map( ... ).toList

Here the map function apply on a lazy IterableView, which output also an IterableView, and the actual construction is triggered by the toList.

Note: Also, not using view can result in a dangerous behavior. Example:

val m = Map(2->2,3->3)

val l = m.map{ case (k,v) => (k/2,v) ).toList
// List((1,3))

val l = m.view.map{ case (k,v) => (k/2,v) ).toList
// List((1,2), (1,3))

Here, omitting the .view make the map output a Map which overrides duplicate keys (and does additional and unnecessary work).

Juh_
  • 14,628
  • 8
  • 59
  • 92