theread.me/_posts/2015-06-06-array-generator-comprehensions.md

179 lines
6.1 KiB
Markdown
Raw Normal View History

2015-06-07 11:29:49 +00:00
---
layout: post
title: "ES7 Array and Generator comprehensions"
date: 2015-06-06 13:47:00
2016-11-02 15:31:37 +00:00
permalink: es7-array-generator-comprehensions/
2016-11-09 12:27:01 +00:00
categories: programming
2017-10-20 17:47:29 +00:00
author: Mahdi
2015-06-07 11:29:49 +00:00
---
2022-07-27 08:48:57 +00:00
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), comprehensions
can replace map and filter.
2015-06-07 11:29:49 +00:00
Generator comprehension brings the same feature to generators, this is a more
2022-07-27 08:48:57 +00:00
useful feature as it removes the need to write new generators for simple
map/filter operations.
2015-06-08 15:43:38 +00:00
2022-07-27 08:48:57 +00:00
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.
2015-06-07 11:29:49 +00:00
2022-07-27 08:48:57 +00:00
Comprehensions are currently only supported by Firefox, use Firefox 30+ or
[Babel](https://babeljs.io/repl/) to run the examples. The Node.js version of
examples using generator `function* ()`s is available at the
[repository](https://github.com/mdibaiee/array-vs-generator) (doesn't require
transpilation, use latest node).
2015-06-07 11:29:49 +00:00
Syntax
======
2015-06-07 11:29:49 +00:00
2015-06-08 15:45:59 +00:00
The syntax is pretty simple, you can only use `for of` and `if` inside comprehensions.
2015-06-07 11:29:49 +00:00
Array comprehensions:
{% highlight javascript %}
let numbers = [1,2,3,4,5];
let even = [ for (n of numbers) if (n % 2 === 0) n ];
// equivalent:
2015-06-08 15:43:38 +00:00
// let even = numbers.filter(n => n % 2 === 0);
2015-06-07 11:29:49 +00:00
console.log(...even); // 2 4
{% endhighlight %}
Generator comprehensions:
{% highlight javascript %}
// yield 0...5
let generator = function* () {
for (let i = 0; i < 6; i++) {
yield i;
}
}
let squared = ( for (n of generator()) n * n );
2015-06-08 15:43:38 +00:00
// equivalent:
2015-06-07 11:29:49 +00:00
// let squared = Array.from(generator()).map(n => n * n);
console.log(...squared); // 0 1 4 9 16 25
{% endhighlight %}
You can also nest comprehensions:
{% highlight javascript %}
// yield 0...5
let generator = function* () {
for (let i = 0; i < 6; i++) {
yield i;
}
}
// yield three numbers after number
let after = function* (number) {
for (let i = 1; i < 4; i++) {
yield number + i;
}
}
2015-06-08 15:43:38 +00:00
// for each number 0...5, yield an array of 3 numbers after it
2015-06-07 11:29:49 +00:00
let nested = ( for (n of generator())
[ for (i of after(n)) i ]
)
console.table(Array.from(nested));
// 1, 2, 3
// 2, 3, 4
// 3, 4, 5
// 4, 5, 6
// 5, 6, 7
// 6, 7, 8
{% endhighlight %}
Laziness
========
2022-07-27 08:48:57 +00:00
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 while generator comprehensions make
it extremely easy to write them — this is a proof of their usefulness.
2015-06-07 11:29:49 +00:00
2022-07-27 08:48:57 +00:00
In programming, laziness means doing nothing until the results are requested or
in simpler terms, avoiding unnecessary work. For example, when you create an
array and map it, the result will be evaluated no matter you need it now or not,
you need the whole thing or a part of it, etc.
2015-06-07 11:29:49 +00:00
Take this example:
{% highlight javascript %}
let bigArray = new Array(100000);
for (let i = 0; i < 100000; i++) {
bigArray[i] = i;
}
let first = bigArray.map(n => n * n)[0];
console.log(first);
{% endhighlight %}
2022-07-27 08:48:57 +00:00
You know what happens here, first, map is evaluated, returning thousands of
squared numbers, then the first element is returned. We must allocate and
evaluate the whole squared array to be able to know about it's first or second
element.
2015-06-07 11:29:49 +00:00
Think of optimizing it, is it possible to get the desired result without storing
2022-07-27 08:48:57 +00:00
temporary arrays in memory? Can we get the first number directly without
consuming a big chunk of memory?
2015-06-07 11:29:49 +00:00
Yes, using generators, Look at this:
{% highlight javascript %}
2015-06-08 15:43:38 +00:00
let bigGenerator = function* () {
2015-06-07 11:29:49 +00:00
for (let i = 0; i < 100000; i++) {
yield i;
}
}
2015-06-08 15:43:38 +00:00
let squared = ( for (n of bigGenerator()) n * n );
2015-06-07 11:29:49 +00:00
console.log(squared.next());
{% endhighlight %}
2022-07-27 08:48:57 +00:00
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()`.
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.
2015-06-07 11:29:49 +00:00
2015-06-08 15:43:38 +00:00
[
![Generator diagram](/img/generator-diagram.png)
](/img/generator-diagram.png)
{% include caption.html text='Calling squared.next() 4 times' %}
2015-06-07 11:29:49 +00:00
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.
2015-06-08 15:43:38 +00:00
It's a lot faster, with less space required, isn't that awesome?
2015-06-07 11:29:49 +00:00
[Repository: mdibaiee/array-vs-generator](https://github.com/mdibaiee/array-vs-generator)
![Array vs Generator performance](/img/array-vs-generator.png)
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:
2015-06-08 15:43:38 +00:00
2015-06-07 11:29:49 +00:00
[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: for...of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of)
[MDN: Iterators and Generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators?redirectlocale=en-US&redirectslug=JavaScript%2FGuide%2FIterators_and_Generators)
[ES6 in Depth: Generators](https://hacks.mozilla.org/2015/05/es6-in-depth-generators/?utm_source=javascriptweekly&utm_medium=email)