In this tenth and final part of the series, we learn about Generators, the most exciting and significant feature of ES6. It builds on features, iterators and iterables covered in previous articles in this series. It is recommended that you review those before reading further.
Generators are not new to programming languages. They date back to 1974, when they were introduced in CLU language. Programming languages like Python have this feature with similar syntax. Generators have two interesting aspects. They look like functions, but behave like iterators. More importantly, they make asynchronous code look like synchronous code. In essence, they are an improvisation over callbacks and promises. If you are a programming language enthusiast, you might have heard about coroutines. Generators are a limited version of coroutines.
To get a full understanding of generators, lets look at them from three perspectives. The first is the control flow, the second is asynchronous programming and the third is the in-built capability that the ES6 runtime provides that makes programmers lives easier (what I refer to as free gifts!).
The concept of generators involves a steep learning curve and might take time to grasp, but that is normal.
Control flow
JavaScript has run-to-completion semantics, which means the sequence of instructions is executed from first to last. Conditional and looping statements change the flow. Functions get executed from the first instruction to the last, except when a return or an exception is encountered inbetween. Generators introduce a mechanism where control is switched back and forth between the calling code and the generator function. The calling code is the controller function. A generator function has multiple entry points. It resumes from where it gave control to the calling function or instruction.
Generators syntax and flow
The program below introduces the syntax of generators. A function is made a generator by adding an * (asterisk) after the keyword function. The flow of the program breaks every time the keyword yield is encountered.
1. function* HelloWorld() { 2. console.log(Start); 3. yield hello; 4. yield world; 5. console.log(End); 6. }
We create an object h of the above generator and start interacting with it. As mentioned earlier, a generator gives control and takes it back using another piece of code. We can call it the generator controller. It could be in your main program or another function.
7. var h = HelloWorld(); 8. console.log(h.next()); 9. console.log(h.next()); 10. console.log(h.next());
You could execute the above program (lines 1 to 10), copying it to a file.
After instantiation of the generator object in Line 7, ensure that Start gets printed only when the first token (Hello) is fetched from Line 8. The function execution gets suspended at Line 3. After invoking another next(), control goes back to the generator function, which executes Line 4. The third invocation of the next() method brings you to the end of strings. So the End string is printed and the end of the iterator is reached.
I strongly recommend that you to try this out in an interactive REPL or command line environment, copying Lines 1 to 7. I use the extension in the Chrome browser called Scratch JS.
> h.next() Start Object { value: hello, done: false } > h.next(); Object { value: world, done: false } > h.next(); End Object { value: undefined, done: true }
Automatic creation of an iterable object
The code example given below shows the Fibonacci programme implemented using a generator:
1. // generator function 2. function* fibonacci() { 3. let [prev, curr] = [0, 1]; 4. for (;;) { 5. [prev, curr] = [curr, prev + curr]; 6. yield curr; 7. } 8. } 9. 10. //controller 11. function controller() { 12. for (let f of fibonacci()) { 13. if (f > 1000) return; 14. console.log(f); 15. } 16. } 17. controller();
Control switches between the generator and the controller. In the above example, you will notice that the generator returns an iterable object and hence can be used in a for..of loop. This is one of the benefits of using a generator. In the previous article on iterators, we learnt that they can be used to create custom iterators, which require careful programming, as they need to maintain the internal state. With generators, the creation of the iterator is implicit and state is maintained.
Passing values to generators and yield
The following example shows the passing of a value to a generator function, just like any other function. It also illustrates the passing of the value from the next() method. This allows the passing of values between generator and controlling code.
1. function *calc(a) { 2. var plus1 = yield (a + 1); 3. var div2 = yield (plus1 / 2); 4. return (a + plus1 + div2); 5. } 6. 7. var iter = calc(1); 8. console.log(iter.next()); 9. console.log(iter.next(2)); 10. console.log(iter.next(3)); 11. The output is: 12. { value: 2, done: false } 13. { value: 1, done: false } 14. { value: 6, done: true }
Asynchronous programming
In JavaScript, there was only a callback mechanism which allowed asynchronous programming. The generators feature takes asynchronous programming to an entirely new level. In the above example, we have seen the keyword yield followed by an expression. The keyword yield can be followed by async functions such as file fetch, URL fetch or database operations.
Compute and storage
Generators help in processing collections or streaming of data, without the need to collect all data. They allow incremental processing as well as lazy evaluation, unfortunately. Lazy evaluation is related to functional programming and is beyond the scope of this article. Programming using generators is preferred when compute is cheaper than storage.
Support matrix
Support for generators is great in the latest versions of Google Chrome, Microsoft Edge and Node.js. The Firefox browser, too, has generator functionality to the greatest extent.
Language continues to evolve
In the last 10 articles in this series, we learnt about most of the important features of ES6. There are many variations and permutations possible with its features. It is left to programmers to explore and exploit the features to write better and more maintainable code. So, continue to use the features and do contribute to standards bodies to make JavaScript the best programming language.