ES7 Array and Generator comprehensions

This commit is contained in:
Mahdi Dibaiee 2015-06-08 20:13:38 +04:30
parent 201586b097
commit 710a8d43cd
3 changed files with 18 additions and 10 deletions

View File

@ -8,10 +8,12 @@ categories: es7, generator, array
Array comprehension is a new feature proposed for ES7, with a new syntax Array comprehension is a new feature proposed for ES7, with a new syntax
to create new arrays from existing [iterables](http://www.2ality.com/2015/02/es6-iteration.html), to create new arrays from existing [iterables](http://www.2ality.com/2015/02/es6-iteration.html),
comprehensions can replace `map`, `filter`. comprehensions can replace map and filter.
Generator comprehension brings the same feature to generators, this is a more Generator comprehension brings the same feature to generators, this is a more
significant change as it removes the need to write new generators for simple map / filter operations. useful feature as it removes the need to write new generators for simple map/filter operations.
Generator comprehensions allow us to easily write single-line generators, which can replace our arrays in some situations, you might ask why we might consider replacing arrays with generators, the most important reason is their [laziness](#laziness). I've explained laziness later in the article.
Comprehensions are currently only supported by Firefox, use Firefox 30+ or [Babel](https://babeljs.io/repl/) to run the examples. The Node.js version using generator `function* ()`s is available at the [repository](https://github.com/mdibaiee/array-vs-generator) (doesn't require transpilation, use latest node). Comprehensions are currently only supported by Firefox, use Firefox 30+ or [Babel](https://babeljs.io/repl/) to run the examples. The Node.js version using generator `function* ()`s is available at the [repository](https://github.com/mdibaiee/array-vs-generator) (doesn't require transpilation, use latest node).
@ -25,7 +27,7 @@ let numbers = [1,2,3,4,5];
let even = [ for (n of numbers) if (n % 2 === 0) n ]; let even = [ for (n of numbers) if (n % 2 === 0) n ];
// equivalent: // equivalent:
// let even = numbers.map(n => { if (n % 2 === 0) return n }); // let even = numbers.filter(n => n % 2 === 0);
console.log(...even); // 2 4 console.log(...even); // 2 4
{% endhighlight %} {% endhighlight %}
@ -40,7 +42,7 @@ let generator = function* () {
} }
let squared = ( for (n of generator()) n * n ); let squared = ( for (n of generator()) n * n );
// equivalent (not lazy): // equivalent:
// let squared = Array.from(generator()).map(n => n * n); // let squared = Array.from(generator()).map(n => n * n);
console.log(...squared); // 0 1 4 9 16 25 console.log(...squared); // 0 1 4 9 16 25
@ -63,7 +65,7 @@ let after = function* (number) {
} }
} }
// for each number of 0...5, yield an array of 3 numbers after it // for each number 0...5, yield an array of 3 numbers after it
let nested = ( for (n of generator()) let nested = ( for (n of generator())
[ for (i of after(n)) i ] [ for (i of after(n)) i ]
) )
@ -77,7 +79,7 @@ console.table(Array.from(nested));
// 6, 7, 8 // 6, 7, 8
{% endhighlight %} {% endhighlight %}
#Lazy #Laziness
This is one of the most important advantages of generators over arrays and things alike. This is one of the most important advantages of generators over arrays and things alike.
The reason why I'm including this here is to give you a good reason to write generators instead of arrays The reason why I'm including this here is to give you a good reason to write generators instead of arrays
while generator comprehensions make it extremely easy to write them — this is a proof of their usefulness. while generator comprehensions make it extremely easy to write them — this is a proof of their usefulness.
@ -106,27 +108,32 @@ temporary arrays in memory? Can we get the first number directly without consumi
Yes, using generators, Look at this: Yes, using generators, Look at this:
{% highlight javascript %} {% highlight javascript %}
let bigArray = function* () { let bigGenerator = function* () {
for (let i = 0; i < 100000; i++) { for (let i = 0; i < 100000; i++) {
yield i; yield i;
} }
} }
let squared = ( for (n of bigArray()) n * n ); let squared = ( for (n of bigGenerator()) n * n );
console.log(squared.next()); console.log(squared.next());
{% endhighlight %} {% endhighlight %}
Let's see what happens in this case. Let's see what happens in this case.
Here, we create a generator which will yield numbers 0...100000, nothing is actually allocated or evaluated, we just have a generator which will return a new number every time we call `next()`. Here, we create a generator which will yield numbers 0...100000, nothing is actually allocated or evaluated, we just have a generator which will return a new number every time we call `next()`.
Then we use generator comprehension to create another generator which squares the numbers our `bigArray()` generator yields, again, we don't evaluate or allocate anything, we just create a generator which will call another generator's `next()` method, square the results, and yield it. Then we use generator comprehension to create another generator which squares the numbers our `bigGenerator()` yields, again, we don't evaluate or allocate anything, we just create a generator which will call another generator's `next()` method, square the results, and yield it.
Now when we call `squared.next()`, the `squared` generator calls `bigArray().next()`, squares the results and yields it, it doesn't do any unnecessary work, it's lazy. Now when we call `squared.next()`, the `squared` generator calls `bigArray().next()`, squares the results and yields it, it doesn't do any unnecessary work, it's lazy.
[
![Generator diagram](/img/generator-diagram.png)
](/img/generator-diagram.png)
{% include caption.html text='Calling squared.next() 4 times' %}
If you profile heap/memory usage and running time, you will see the difference. If you profile heap/memory usage and running time, you will see the difference.
I have prepared a Node.js version of the test case. With the help of [`process.memoryUsage()`](https://nodejs.org/api/process.html#process_process_memoryusage) and [`console.time`](https://developer.mozilla.org/en-US/docs/Web/API/Console/time) we can easily see the difference. I have prepared a Node.js version of the test case. With the help of [`process.memoryUsage()`](https://nodejs.org/api/process.html#process_process_memoryusage) and [`console.time`](https://developer.mozilla.org/en-US/docs/Web/API/Console/time) we can easily see the difference.
It's about ten times faster with two times less space used, isn't that awesome? It's a lot faster, with less space required, isn't that awesome?
[Repository: mdibaiee/array-vs-generator](https://github.com/mdibaiee/array-vs-generator) [Repository: mdibaiee/array-vs-generator](https://github.com/mdibaiee/array-vs-generator)
@ -135,6 +142,7 @@ It's about ten times faster with two times less space used, isn't that awesome?
If you want to know more about lazy iterators, I recommend raganwald's [Lazy Iterables in JavaScript](http://raganwald.com/2015/02/17/lazy-iteratables-in-javascript.html). If you want to know more about lazy iterators, I recommend raganwald's [Lazy Iterables in JavaScript](http://raganwald.com/2015/02/17/lazy-iteratables-in-javascript.html).
More: More:
[MDN: Array Comprehensions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Array_comprehensions) [MDN: Array Comprehensions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Array_comprehensions)
[MDN: Generator Comprehensions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Generator_comprehensions) [MDN: Generator Comprehensions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Generator_comprehensions)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 92 KiB

BIN
img/generator-diagram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB