0

This is a question about the Scala compiler.

Let's say I have a List, and that I transform that list through several maps and flatMaps.

val myList = List(1,2,3,4,5)

val transformed = myList.map(_+1).map(_*2).flatmap(x=>List(x-1,x,x+1))

Let's say I transform it some more.

val moreTransformed = transformed.map(_-2).map(_/5).flatMap(x=>List(x/2,x*2))

My question can be divided into two parts

  1. When generating the val for transformed, does the underlying Java bytecode create the intermediate lists? I am referring to the successive calls to map and flatMap in the computation of transformed. Can the Scala compiler compose these calls into a single flatMap? If I was operating on a list of objects, this would entail creation of fewer intermediate objects. If the compiler is naive and simply creates the intermediate objects, that could potentially result in considerable overhead for computations that involve long chains of map and flatMap.

  2. Let us say that of the two vals created above, I only use moreTransformed (the second val) in further computation. That is, I only use transformed (the first val) in the calculation of moreTransformed and nowhere else. Is the Scala compiler smart enough not to create the List for transformed and to compute only moreTransformed? Is it smart enough to compose all the functions in transformed and moreTransformed so that only a single List, the value of moreTransformed, is produced?

Allen Han
  • 1,163
  • 7
  • 16
  • 2
    Additionally to what has been already told. You shouldn't really care about this. - Unless, you have benchmarked and identify those as bottlenecks. I'm that case, there are many optimizations one may use. - For the first example, start the chain with `.iterator` and end it with ` toList`, that way it will be just one iteration. - for the second case, consider a **LazyList** - Finally, if all your program is a big data pipeline, consider using **Streaming** _(`AkkaStreams`, `fs2`, `monix.Observable` or `zio.ZStream`)_ or if the data is too big, consider `Spark` or `Flink`. – Luis Miguel Mejía Suárez Aug 17 '19 at 12:47

2 Answers2

2

I don't know for sure, what kind of byte code the compiler generates. I'll try to answer it in terms on concept

If I was operating on a list of objects, this would entail creation of fewer intermediate objects. If the compiler is naive and simply creates the intermediate objects, that could potentially result in considerable overhead for computations that involve long chains of map and flatMap.

Yes. Scala's collection List is by default strict, meaning all intermediate objects required will be computed and generated.

Is the Scala compiler smart enough not to create the List for transformed and to compute only moreTransformed?

Short answer, No

Is it smart enough to compose all the functions in transformed and moreTransformed so that only a single List, the value of moreTransformed, is produced?

No

If you want the features mentioned in the 2nd and 3rd block, there's LazyList and Stream API. In general lazy collections are particularly useful to describe successive transformation operations without evaluating intermediate transformations.

You can read a brief overview about strict and lazy evaluation here.

Some exercise on lazy evaluation here

1565986223
  • 6,420
  • 2
  • 20
  • 33
2

It doesn't matter how smart the compiler is, it must still conform to what the language specifies.

In this case the language says that each map/flatMap operation must appear to complete before the next one starts. So the compiler can only perform the optimisations that you mention if it can guarantee that the behaviour will be the same.

In the specific case of the example in the question, the compiler knows what is in myList, and the functions that are applied have very clear semantics. The compiler could theoretically optimise this and pre-compute the results without doing anything at run time.

In the more general case the compiler will not know what is in myList and the operations will have a chance of failing. In this case the compiler has no choice but to execute each operation in turn. That is the only way to guarantee the correct results according to the language.


Note that the Scala code is usually executed in a JVM with a JIT compiler, and that is where much of the optimisation is done. The sequential map calls will be converted into sequential loops in the bytecode and in some circumstances the JIT compiler may be able to combine those loops into a single loop. However this optimisation cannot be done if there is anything in the loop with a side effect, including object allocation.

Tim
  • 26,753
  • 2
  • 16
  • 29
  • 1
    Note that even in Haskell, which has more restrictive semantics (ergo, more information for the compiler), and much more "research-y" compilers, *map fusion* is actually a manually implemented optimization at least in GHC: https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#rewrite-rules *Rewrite Rules* are rules you add to the source code, which act as pragmas to the compiler and tell it "if you see *this* pattern of source code, you can actually replace it with *that* pattern". So, IOW, the compiler is not smart enough by itself to make this optimization, … – Jörg W Mittag Aug 17 '19 at 15:27
  • 1
    … rather, there is a pragma in the source code of the standard library `map` implementation which tells the compiler that `(map f) . (map g)` is `map (f . g)` – Jörg W Mittag Aug 17 '19 at 15:29