Calling map on a parallel collection via a reference to an ancestor type

坚强是说给别人听的谎言 提交于 2019-12-01 13:25:28

The explanation of why it happens is a long story: in Scala 2.9.x (I don't know about the other versions) those collections methods such as filter or map relies on the CanBuildFrom mechanism. The idea is that you have an implicit parameter which is used to create a builder for the new collection:

def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = {
    val b = bf(repr)
    b.sizeHint(this) 
    for (x <- this) b += f(x)
    b.result
  }

Thanks to this mechanism, the map method is defined only in the TraversableLike trait and its subclasses do not need to override it. As you see, inside the method map signature there are many type parameters. Let's look at the trivial ones:

  • The B which is the type of the elements of the new collection
  • The A is the type of the elements in the source collection

Let's look at the more complicated ones:

  • That is the new type of collection, which can be different from the current type. A classical example is when you map for example a BitSet using a toString:

     scala> val a = BitSet(1,3,5)
     a scala.collection.immutable.BitSet = BitSet(1, 3, 5)
     scala>  a.map {_.toString}
     res2: scala.collection.immutable.Set[java.lang.String] = Set(1, 3, 5)
    

Since it is illegal to create a BitSet[String] your map result will be a Set[String] Finally Repr is the type of the current collection. When you try to map a collection over a function, the compiler will resolve a suitable CanBuildFrom using the type parameters.

As it is reasonable, the map method has been overridden in parallel collections in ParIterableLike as the following:

 def map[S, That](f: T => S)(implicit bf: CanBuildFrom[Repr, S, That]): That = bf ifParallel { pbf =>
    executeAndWaitResult(new Map[S, That](f, pbf, splitter) mapResult { _.result })
  } otherwise seq.map(f)(bf2seq(bf))

As you can see the method has the same signature, but it uses a different approach: it test whether the provided CanBuildFrom is parallel and otherwise falls back on the default implementation. Therefore, Scala parallel collections use special CanBuildFrom (parallel ones) which create parallel builders for the map methods.

However, what happens when you do

(if(runParallel) theList.par else theList) map println //Doesn't run in parallel

is the map method gets executed on the result of

  (if(runParallel) theList.par else theList) 

whose return type is the first common ancestors of both classes (in this case just a certain number of traits mixed togethers). Since it is a common ancestor, is type parameter Repr will be some kind of common ancestors of both collections representation, let's call it Repr1.


Conclusion

When you call the map method, the compiler should find a suitable CanBuildFrom[Repr, B, That] for the operation. Since our Repr1 is not the one of a parallel collection, there won't be any CanBuildFrom[Repr1,B,That] capable of providing a parallel builder. This is actually a correct behaviour with respect to the implementation of Scala collections, if the behaviour would be different that would mean that every map of non parallel collections would be run in parallel as well.

The point here is that, for how Scala collections are designed in 2.9.x there is no alternative. If the compiler does not provide a CanBuildFrom for a parallel collection, the map won't be parallel.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!