2

Let consider a simple use of map() function in JavaScript:

[1,2].map(x => x + 1).map(x => x + 2); // [3,4]

I am wondering whether each of arrow function calls is executed in separate loops or if the functions are applied to array elements in one loop. I know that for example in java those function would be executed in one loop (using Streams from Java 8). In that case we can chain some functions that are non-terminal (they return Stream on which we can call another function) but those operations are not executed until we use a terminal function (like collect() that transforms Stream into some collection, like an array). Thus, all the operations (maps, filters etc.) are merged so that all of them can run in one loop.

On the other hand we have a JavaScript where we know that Array.prototype.map() returns another array, so it seems like each map() call is executed in separate loop. But that would be quite a waste of execution time! The simplest answer is that it's all optimized by engine so the functions are indeed executed in one loop, but I couldn't find any information that this is the case.

A side question - can we say that Java's Stream is a functor? It seems like it is - it has a map function that transforms collection to same-size collection and returns a stream. But on the other hand, as mentioned before, this map function does not in fact modify anything until we call some terminal function.

zuroslav
  • 197
  • 1
  • 7
  • 1
    What would make you believe that it does? It has to return an Iterable before the second Map function is capable of running on it. – Fallenreaper Dec 01 '17 at 03:28
  • It is more about what actually happens - as I said it seems like a waste of execution time and maybe engines optimize those calls, but I really don't know if this is the case. – zuroslav Dec 01 '17 at 03:31
  • 4
    @Fallenreaper: In theory, a ridiculously heavily optimizing JIT that can determine both mapping functions are side-effect free might be able to combine them into a single operation, since the intermediate is unused. That said, I highly doubt even the latest browsers' JS engines are going to that amount of trouble; the analysis necessary to recognize and optimize that case would be complicated and probably run-time expensive, and complex JIT-ing optimizations are hard to justify for limited use cases. – ShadowRanger Dec 01 '17 at 03:32
  • In theory sure, but you should be able to see the source code of the maps which would execute. Though im not generally the type of person who likes to see people talking about javascript and reference Java implementations. That being said, Source Docs of Map should tell you what is going on, and while yes things could become optimized a bit on a browser side, how would Firefox monitor that "Oh, ahead is a double map, lets meld them together." I like to think of it as WYSIWYG, read the docs. – Fallenreaper Dec 01 '17 at 03:36
  • @zuroslav – a beautiful reduction-of-reductions technique exists; called a *transducer* by some . I have a [couple answers](https://stackoverflow.com/search?q=user%3A633183+transducers) here talking about them – Mulan Dec 01 '17 at 04:26
  • 1
    Related: [Do Immutable.js or Lazy.js perform short-cut fusion?](https://stackoverflow.com/q/27529486/1048572) – Bergi Dec 01 '17 at 06:57

2 Answers2

1

When dealing with functors you have laws that you can rely on to help you refactor and optimise your code whenever you see a pattern.

In this instance when you are using map(a).map(b) you can convert it to map(compose(b, a))

Using compose will run a first passing its return value to b while in a single loop. Using map map you will loop the array twice.

const compose = (f, g) => x => f(g(x))
const add1 = x => x + 1
const add2 = x => x + 2

console.log(
  [1,2].map(add1).map(add2)
)

console.log(
  [1, 2].map(compose(add2, add1))
)
<script src="https://codepen.io/synthet1c/pen/KyQQmL.js"></script>
synthet1c
  • 6,152
  • 2
  • 24
  • 39
  • 2
    Note that this transformation is only appropriate if the callbacks have no side effects or we don't care about the order of the side effects. The original code will execute all the side effects of the first map before any of the second map, while the rewrite will interleave them. – Barmar Dec 01 '17 at 04:08
1

When chaining map functions - are they executed in one loop?

[1,2].map(x => x + 1).map(x => x + 2); // [3,4]

The answer is no, the underlying engine does not do this sort of optimisation, the code is actually executed the way you see it. Unlikely other languages such Haskell where features like Short cut fusion are a key part of its success.


However, this does not mean you cannot achieve a similar level of optimisation in javascript. :) This is why I'd like to introduce the concept of Transducer, I'd also suggest you to read this medium article.

So let's review your example above, the order of your algorithm is O(2n), meaning that we perform 2 operations per each item in the original input. Thus, between the first map and the second you produce intermediary values, which to some extent also badly impact in performances.

Why do transducers come to the rescue here?
  • They let you preserve the same level of readability given by separating concerns and having small and dedicated single-purpose functions
  • Applies short cut fusion optimisations

const algorithm = R.compose(
  R.map(R.inc),
  R.map(R.multiply(2)),
);

const fn = R.into([], algorithm);

console.log(
  fn([1, 2, 3, 4]),
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js" integrity="sha512-3sdB9mAxNh2MIo6YkY05uY1qjkywAlDfCf5u1cSotv6k9CZUSyHVf4BJSpTYgla+YHLaHG8LUpqV7MHctlYzlw==" crossorigin="anonymous"></script>
Hitmands
  • 13,491
  • 4
  • 34
  • 69