Movatterモバイル変換


[0]ホーム

URL:


22. Generators
Table of contents
Please support this book:buy it (PDF, EPUB, MOBI) ordonate
(Ad, please don’t block.)

22.Generators



The following GitHub repository contains the example code:generator-examples

22.1Overview

22.1.1What are generators?

You can think of generators as processes (pieces of code) that you can pause and resume:

function*genFunc(){// (A)console.log('First');yield;console.log('Second');}

Note the new syntax:function* is a new “keyword” forgenerator functions (there are alsogenerator methods).yield is an operator with which a generator can pause itself. Additionally, generators can also receive input and send output viayield.

When you call a generator functiongenFunc(), you get agenerator objectgenObj that you can use to control the process:

constgenObj=genFunc();

The process is initially paused in line A.genObj.next() resumes execution, ayield insidegenFunc() pauses execution:

genObj.next();// Output: FirstgenObj.next();// output: Second

22.1.2Kinds of generators

There are four kinds of generators:

  1. Generator function declarations:
    function*genFunc(){···}constgenObj=genFunc();
  2. Generator function expressions:
    constgenFunc=function*(){···};constgenObj=genFunc();
  3. Generator method definitions in object literals:
    constobj={*generatorMethod(){···}};constgenObj=obj.generatorMethod();
  4. Generator method definitions in class definitions (class declarations or class expressions):
    classMyClass{*generatorMethod(){···}}constmyInst=newMyClass();constgenObj=myInst.generatorMethod();

22.1.3Use case: implementing iterables

The objects returned by generators are iterable; eachyield contributes to the sequence of iterated values. Therefore, you can use generators to implement iterables, which can be consumed by various ES6 language mechanisms:for-of loop, spread operator (...), etc.

The following function returns an iterable over the properties of an object, one [key, value] pair per property:

function*objectEntries(obj){constpropKeys=Reflect.ownKeys(obj);for(constpropKeyofpropKeys){// `yield` returns a value and then pauses// the generator. Later, execution continues// where it was previously paused.yield[propKey,obj[propKey]];}}

objectEntries() is used like this:

constjane={first:'Jane',last:'Doe'};for(const[key,value]ofobjectEntries(jane)){console.log(`${key}:${value}`);}// Output:// first: Jane// last: Doe

How exactlyobjectEntries() works is explained ina dedicated section. Implementing the same functionality without generators is much more work.

22.1.4Use case: simpler asynchronous code

You can use generators to tremendously simplify working with Promises. Let’s look at a Promise-based functionfetchJson() and how it can be improved via generators.

functionfetchJson(url){returnfetch(url).then(request=>request.text()).then(text=>{returnJSON.parse(text);}).catch(error=>{console.log(`ERROR:${error.stack}`);});}

Withthe library co and a generator, this asynchronous code looks synchronous:

constfetchJson=co.wrap(function*(url){try{letrequest=yieldfetch(url);lettext=yieldrequest.text();returnJSON.parse(text);}catch(error){console.log(`ERROR:${error.stack}`);}});

ECMAScript 2017 will have async functions which are internally based on generators. With them, the code looks like this:

asyncfunctionfetchJson(url){try{letrequest=awaitfetch(url);lettext=awaitrequest.text();returnJSON.parse(text);}catch(error){console.log(`ERROR:${error.stack}`);}}

All versions can be invoked like this:

fetchJson('http://example.com/some_file.json').then(obj=>console.log(obj));

22.1.5Use case: receiving asynchronous data

Generators can receive input fromnext() viayield. That means that you can wake up a generator whenever new data arrives asynchronously and to the generator it feels like it receives the data synchronously.

22.2What are generators?

Generators are functions that can be paused and resumed (think cooperative multitasking or coroutines), which enables a variety of applications.

As a first example, consider the following generator function whose name isgenFunc:

function*genFunc(){// (A)console.log('First');yield;// (B)console.log('Second');// (C)}

Two things distinguishgenFunc from a normal function declaration:

CallinggenFunc does not execute its body. Instead, you get a so-calledgenerator object, with which you can control the execution of the body:

> const genObj = genFunc();

genFunc() is initially suspended before the body (line A). The method callgenObj.next() continues execution until the nextyield:

> genObj.next()First{ value: undefined, done: false }

As you can see in the last line,genObj.next() also returns an object. Let’s ignore that for now. It will matter later.

genFunc is now paused in line B. If we callnext() again, execution resumes and line C is executed:

> genObj.next()Second{ value: undefined, done: true }

Afterwards, the function is finished, execution has left the body and further calls ofgenObj.next() have no effect.

22.2.1Roles played by generators

Generators can play three roles:

  1. Iterators (data producers): Eachyield can return a value vianext(), which means that generators can produce sequences of values via loops and recursion. Due to generator objects implementing the interfaceIterable (which is explained inthe chapter on iteration), these sequences can be processed by any ECMAScript 6 construct that supports iterables. Two examples are:for-of loops and the spread operator (...).
  2. Observers (data consumers):yield can also receive a value fromnext() (via a parameter). That means that generators become data consumers that pause until a new value is pushed into them vianext().
  3. Coroutines (data producers and consumers): Given that generators are pausable and can be both data producers and data consumers, not much work is needed to turn them into coroutines (cooperatively multitasked tasks).

The next sections provide deeper explanations of these roles.

22.3Generators as iterators (data production)

For this section, you should be familiar with ES6 iteration.The previous chapter has more information.

As explained before, generator objects can be data producers, data consumers or both. This section looks at them as data producers, where they implement both the interfacesIterable andIterator (shown below). That means that the result of a generator function is both an iterable and an iterator. The full interface of generator objects will be shown later.

interfaceIterable{[Symbol.iterator]():Iterator;}interfaceIterator{next():IteratorResult;}interfaceIteratorResult{value:any;done:boolean;}

I have omitted methodreturn() of interfaceIterable, because it is not relevant in this section.

A generator function produces a sequence of values viayield, a data consumer consumes thoses values via the iterator methodnext(). For example, the following generator function produces the values'a' and'b':

function*genFunc(){yield'a';yield'b';}

This interaction shows how to retrieve the yielded values via the generator objectgenObj:

> const genObj = genFunc();> genObj.next(){ value: 'a', done: false }> genObj.next(){ value: 'b', done: false }> genObj.next() // done: true => end of sequence{ value: undefined, done: true }

22.3.1Ways of iterating over a generator

As generator objects are iterable, ES6 language constructs that support iterables can be applied to them. The following three ones are especially important.

First, thefor-of loop:

for(constxofgenFunc()){console.log(x);}// Output:// a// b

Second, the spread operator (...), which turns iterated sequences into elements of an array (consultthe chapter on parameter handling for more information on this operator):

constarr=[...genFunc()];// ['a', 'b']

Third, destructuring:

> const [x, y] = genFunc();> x'a'> y'b'

22.3.2Returning from a generator

The previous generator function did not contain an explicitreturn. An implicitreturn is equivalent to returningundefined. Let’s examine a generator with an explicitreturn:

function*genFuncWithReturn(){yield'a';yield'b';return'result';}

The returned value shows up in the last object returned bynext(), whose propertydone istrue:

> const genObjWithReturn = genFuncWithReturn();> genObjWithReturn.next(){ value: 'a', done: false }> genObjWithReturn.next(){ value: 'b', done: false }> genObjWithReturn.next(){ value: 'result', done: true }

However, most constructs that work with iterables ignore the value inside thedone object:

for(constxofgenFuncWithReturn()){console.log(x);}// Output:// a// bconstarr=[...genFuncWithReturn()];// ['a', 'b']

yield*, an operator for making recursive generator calls, does consider values insidedone objects. It is explained later.

22.3.3Throwing an exception from a generator

If an exception leaves the body of a generator thennext() throws it:

function*genFunc(){thrownewError('Problem!');}constgenObj=genFunc();genObj.next();// Error: Problem!

That means thatnext() can produce three different “results”:

22.3.4Example: iterating over properties

Let’s look at an example that demonstrates how convenient generators are for implementing iterables. The following function,objectEntries(), returns an iterable over the properties of an object:

function*objectEntries(obj){// In ES6, you can use strings or symbols as property keys,// Reflect.ownKeys() retrieves bothconstpropKeys=Reflect.ownKeys(obj);for(constpropKeyofpropKeys){yield[propKey,obj[propKey]];}}

This function enables you to iterate over the properties of an objectjane via thefor-of loop:

constjane={first:'Jane',last:'Doe'};for(const[key,value]ofobjectEntries(jane)){console.log(`${key}:${value}`);}// Output:// first: Jane// last: Doe

For comparison – an implementation ofobjectEntries() that doesn’t use generators is much more complicated:

functionobjectEntries(obj){letindex=0;letpropKeys=Reflect.ownKeys(obj);return{[Symbol.iterator](){returnthis;},next(){if(index<propKeys.length){letkey=propKeys[index];index++;return{value:[key,obj[key]]};}else{return{done:true};}}};}

22.3.5You can onlyyield in generators

A significant limitation of generators is that you can only yield while you are (statically) inside a generator function. That is, yielding in callbacks doesn’t work:

function*genFunc(){['a','b'].forEach(x=>yieldx);// SyntaxError}

yield is not allowed inside non-generator functions, which is why the previous code causes a syntax error. In this case, it is easy to rewrite the code so that it doesn’t use callbacks (as shown below). But unfortunately that isn’t always possible.

function*genFunc(){for(constxof['a','b']){yieldx;// OK}}

The upside of this limitation isexplained later: it makes generators easier to implement and compatible with event loops.

22.3.6Recursion viayield*

You can only useyield within a generator function. Therefore, if you want to implement a recursive algorithm with generator, you need a way to call one generator from another one. This section shows that that is more complicated than it sounds, which is why ES6 has a special operator,yield*, for this. For now, I only explain howyield* works if both generators produce output, I’ll later explain how things work if input is involved.

How can one generator recursively call another generator? Let’s assume you have written a generator functionfoo:

function*foo(){yield'a';yield'b';}

How would you callfoo from another generator functionbar? The following approach does not work!

function*bar(){yield'x';foo();// does nothing!yield'y';}

Callingfoo() returns an object, but does not actually executefoo(). That’s why ECMAScript 6 has the operatoryield* for making recursive generator calls:

function*bar(){yield'x';yield*foo();yield'y';}// Collect all values yielded by bar() in an arrayconstarr=[...bar()];// ['x', 'a', 'b', 'y']

Internally,yield* works roughly as follows:

function*bar(){yield'x';for(constvalueoffoo()){yieldvalue;}yield'y';}

The operand ofyield* does not have to be a generator object, it can be any iterable:

function*bla(){yield'sequence';yield*['of','yielded'];yield'values';}constarr=[...bla()];// ['sequence', 'of', 'yielded', 'values']
22.3.6.1yield* considers end-of-iteration values

Most constructs that support iterables ignore the value included in the end-of-iteration object (whose propertydone istrue). Generators provide that value viareturn. The result ofyield* is the end-of-iteration value:

function*genFuncWithReturn(){yield'a';yield'b';return'The result';}function*logReturned(genObj){constresult=yield*genObj;console.log(result);// (A)}

If we want to get to line A, we first must iterate over all values yielded bylogReturned():

> [...logReturned(genFuncWithReturn())]The result[ 'a', 'b' ]
22.3.6.2Iterating over trees

Iterating over a tree with recursion is simple, writing an iterator for a tree with traditional means is complicated. That’s why generators shine here: they let you implement an iterator via recursion. As an example, consider the following data structure for binary trees. It is iterable, because it has a method whose key isSymbol.iterator. That method is a generator method and returns an iterator when called.

classBinaryTree{constructor(value,left=null,right=null){this.value=value;this.left=left;this.right=right;}/** Prefix iteration */*[Symbol.iterator](){yieldthis.value;if(this.left){yield*this.left;// Short for: yield* this.left[Symbol.iterator]()}if(this.right){yield*this.right;}}}

The following code creates a binary tree and iterates over it viafor-of:

consttree=newBinaryTree('a',newBinaryTree('b',newBinaryTree('c'),newBinaryTree('d')),newBinaryTree('e'));for(constxoftree){console.log(x);}// Output:// a// b// c// d// e

22.4Generators as observers (data consumption)

As consumers of data, generator objects conform to the second half of the generator interface,Observer:

interfaceObserver{next(value?:any):void;return(value?:any):void;throw(error):void;}

As an observer, a generator pauses until it receives input. There are three kinds of input, transmitted via the methods specified by the interface:

22.4.1Sending values vianext()

If you use a generator as an observer, you send values to it vianext() and it receives those values viayield:

function*dataConsumer(){console.log('Started');console.log(`1.${yield}`);// (A)console.log(`2.${yield}`);return'result';}

Let’s use this generator interactively. First, we create a generator object:

> const genObj = dataConsumer();

We now callgenObj.next(), which starts the generator. Execution continues until the firstyield, which is where the generator pauses. The result ofnext() is the value yielded in line A (undefined, becauseyield doesn’t have an operand). In this section, we are not interested in whatnext() returns, because we only use it to send values, not to retrieve values.

>genObj.next()Started{value:undefined,done:false}

We callnext() two more times, in order to send the value'a' to the firstyield and the value'b' to the secondyield:

>genObj.next('a')1.a{value:undefined,done:false}>genObj.next('b')2.b{value:'result',done:true}

The result of the lastnext() is the value returned fromdataConsumer().done beingtrue indicates that the generator is finished.

Unfortunately,next() is asymmetric, but that can’t be helped: It always sends a value to the currently suspendedyield, but returns the operand of the followingyield.

22.4.1.1The firstnext()

When using a generator as an observer, it is important to note that the only purpose of the first invocation ofnext() is to start the observer. It is only ready for input afterwards, because this first invocation advances execution to the firstyield. Therefore, any input you send via the firstnext() is ignored:

function*gen(){// (A)while(true){constinput=yield;// (B)console.log(input);}}constobj=gen();obj.next('a');obj.next('b');// Output:// b

Initially, execution is paused in line A. The first invocation ofnext():

The second invocation ofnext():

The following utility function fixes this issue:

/** * Returns a function that, when called, * returns a generator object that is immediately * ready for input via `next()` */functioncoroutine(generatorFunction){returnfunction(...args){constgeneratorObject=generatorFunction(...args);generatorObject.next();returngeneratorObject;};}

To see howcoroutine() works, let’s compare a wrapped generator with a normal one:

constwrapped=coroutine(function*(){console.log(`First input:${yield}`);return'DONE';});constnormal=function*(){console.log(`First input:${yield}`);return'DONE';};

The wrapped generator is immediately ready for input:

> wrapped().next('hello!')First input: hello!

The normal generator needs an extranext() until it is ready for input:

> const genObj = normal();> genObj.next(){ value: undefined, done: false }> genObj.next('hello!')First input: hello!{ value: 'DONE', done: true }

22.4.2yield binds loosely

yield binds very loosely, so that we don’t have to put its operand in parentheses:

yielda+b+c;

This is treated as:

yield(a+b+c);

Not as:

(yielda)+b+c;

As a consequence, many operators bind more tightly thanyield and you have to putyield in parentheses if you want to use it as an operand. For example, you get a SyntaxError if you make an unparenthesizedyield an operand of plus:

console.log('Hello'+yield);// SyntaxErrorconsole.log('Hello'+yield123);// SyntaxErrorconsole.log('Hello'+(yield));// OKconsole.log('Hello'+(yield123));// OK

You do not need parens ifyield is a direct argument in a function or method call:

foo(yield'a',yield'b');

You also don’t need parens if you useyield on the right-hand side of an assignment:

constinput=yield;
22.4.2.1yield in the ES6 grammar

The need for parens aroundyield can be seen in the following grammar rules in theECMAScript 6 specification. These rules describe how expressions are parsed. I list them here from general (loose binding, lower precedence) to specific (tight binding, higher precedence). Wherever a certain kind of expression is demanded, you can also use more specific ones. The opposite is not true. The hierarchy ends withParenthesizedExpression, which means that you can mention any expression anywhere, if you put it in parentheses.

Expression :    AssignmentExpression    Expression , AssignmentExpressionAssignmentExpression :    ConditionalExpression    YieldExpression    ArrowFunction    LeftHandSideExpression = AssignmentExpression    LeftHandSideExpression AssignmentOperator AssignmentExpression···AdditiveExpression :    MultiplicativeExpression    AdditiveExpression + MultiplicativeExpression    AdditiveExpression - MultiplicativeExpressionMultiplicativeExpression :    UnaryExpression    MultiplicativeExpression MultiplicativeOperator UnaryExpression···PrimaryExpression :    this    IdentifierReference    Literal    ArrayLiteral    ObjectLiteral    FunctionExpression    ClassExpression    GeneratorExpression    RegularExpressionLiteral    TemplateLiteral    ParenthesizedExpressionParenthesizedExpression :    ( Expression )

The operands of anAdditiveExpression are anAdditiveExpression and aMultiplicativeExpression. Therefore, using a (more specific)ParenthesizedExpression as an operand is OK, but using a (more general)YieldExpression isn’t.

22.4.3return() andthrow()

Generator objects have two additional methods,return() andthrow(), that are similar tonext().

Let’s recap hownext(x) works (after the first invocation):

  1. The generator is currently suspended at ayield operator.
  2. Send the valuex to thatyield, which means that it evaluates tox.
  3. Proceed to the nextyield,return orthrow:
    • yield x leads tonext() returning with{ value: x, done: false }
    • return x leads tonext() returning with{ value: x, done: true }
    • throw err (not caught inside the generator) leads tonext() throwingerr.

return() andthrow() work similarly tonext(), but they do something different in step 2:

22.4.4return() terminates the generator

return() performs areturn at the location of theyield that led to the last suspension of the generator. Let’s use the following generator function to see how that works.

function*genFunc1(){try{console.log('Started');yield;// (A)}finally{console.log('Exiting');}}

In the following interaction, we first usenext() to start the generator and to proceed until theyield in line A. Then we return from that location viareturn().

> const genObj1 = genFunc1();> genObj1.next()Started{ value: undefined, done: false }> genObj1.return('Result')Exiting{ value: 'Result', done: true }
22.4.4.1Preventing termination

You can preventreturn() from terminating the generator if you yield inside thefinally clause (using areturn statement in that clause is also possible):

function*genFunc2(){try{console.log('Started');yield;}finally{yield'Not done, yet!';}}

This time,return() does not exit the generator function. Accordingly, the propertydone of the object it returns isfalse.

> const genObj2 = genFunc2();> genObj2.next()Started{ value: undefined, done: false }> genObj2.return('Result'){ value: 'Not done, yet!', done: false }

You can invokenext() one more time. Similarly to non-generator functions, the return value of the generator function is the value that was queued prior to entering thefinally clause.

> genObj2.next(){ value: 'Result', done: true }
22.4.4.2Returning from a newborn generator

Returning a value from anewborn generator (that hasn’t started yet) is allowed:

> function* genFunc() {}> genFunc().return('yes'){ value: 'yes', done: true }

Further reading

return() is also used to close iterators. The chapter on iteration hasa detailed section on that.

22.4.5throw() signals an error

throw() throws an exception at the location of theyield that led to the last suspension of the generator. Let’s examine how that works via the following generator function.

function*genFunc1(){try{console.log('Started');yield;// (A)}catch(error){console.log('Caught: '+error);}}

In the following interaction, we first usenext() to start the generator and proceed until theyield in line A. Then we throw an exception from that location.

>constgenObj1=genFunc1();>genObj1.next()Started{value:undefined,done:false}>genObj1.throw(newError('Problem!'))Caught:Error:Problem!{value:undefined,done:true}

The result ofthrow() (shown in the last line) stems from us leaving the function with an implicitreturn.

22.4.5.1Throwing from a newborn generator

Throwing an exception in anewborn generator (that hasn’t started yet) is allowed:

> function* genFunc() {}> genFunc().throw(new Error('Problem!'))Error: Problem!

22.4.6Example: processing asynchronously pushed data

The fact that generators-as-observers pause while they wait for input makes them perfect for on-demand processing of data that is received asynchronously. The pattern for setting up a chain of generators for processing is as follows:

The whole chain is prefixed by a non-generator function that makes an asynchronous request and pushes the results into the chain of generators vianext().

As an example, let’s chain generators to process a file that is read asynchronously.

The code of this example is in the filegenerator-examples/node/readlines.js. It must be executed viababel-node.

The following code sets up the chain: it contains the generatorssplitLines,numberLines andprintLines. Data is pushed into the chain via the non-generator functionreadFile.

readFile(fileName,splitLines(numberLines(printLines())));

I’ll explain what these functions do when I show their code.

As previously explained, if generators receive input viayield, the first invocation ofnext() on the generator object doesn’t do anything. That’s why I usethe previously shown helper functioncoroutine() to create coroutines here. It executes the firstnext() for us.

readFile() is the non-generator function that starts everything:

import{createReadStream}from'fs';/** * Creates an asynchronous ReadStream for the file whose name * is `fileName` and feeds it to the generator object `target`. * * @see ReadStream https://nodejs.org/api/fs.html#fs_class_fs_readstream */functionreadFile(fileName,target){constreadStream=createReadStream(fileName,{encoding:'utf8',bufferSize:1024});readStream.on('data',buffer=>{conststr=buffer.toString('utf8');target.next(str);});readStream.on('end',()=>{// Signal end of output sequencetarget.return();});}

The chain of generators starts withsplitLines:

/** * Turns a sequence of text chunks into a sequence of lines * (where lines are separated by newlines) */constsplitLines=coroutine(function*(target){letprevious='';try{while(true){previous+=yield;leteolIndex;while((eolIndex=previous.indexOf('\n'))>=0){constline=previous.slice(0,eolIndex);target.next(line);previous=previous.slice(eolIndex+1);}}}finally{// Handle the end of the input sequence// (signaled via `return()`)if(previous.length>0){target.next(previous);}// Signal end of output sequencetarget.return();}});

Note an important pattern:

The next generator isnumberLines:

//***Prefixesnumberstoasequenceoflines*/constnumberLines=coroutine(function*(target){try{for(constlineNo=0;;lineNo++){constline=yield;target.next(`${lineNo}:${line}`);}}finally{// Signal end of output sequencetarget.return();}});

The last generator isprintLines:

/** * Receives a sequence of lines (without newlines) * and logs them (adding newlines). */constprintLines=coroutine(function*(){while(true){constline=yield;console.log(line);}});

The neat thing about this code is that everything happens lazily (on demand): lines are split, numbered and printed as they arrive; we don’t have to wait for all of the text before we can start printing.

22.4.7yield*: the full story

As a rough rule of thumb,yield* performs (the equivalent of) a function call from one generator (thecaller) to another generator (thecallee).

So far, we have only seen one aspect ofyield: it propagates yielded values from the callee to the caller. Now that we are interested in generators receiving input, another aspect becomes relevant:yield* also forwards input received by the caller to the callee. In a way, the callee becomes the active generator and can be controlled via the caller’s generator object.

22.4.7.1Example:yield* forwardsnext()

The following generator functioncaller() invokes the generator functioncallee() viayield*.

function*callee(){console.log('callee: '+(yield));}function*caller(){while(true){yield*callee();}}

callee logs values received vianext(), which allows us to check whether it receives the value'a' and'b' that we send tocaller.

> const callerObj = caller();> callerObj.next() // start{ value: undefined, done: false }> callerObj.next('a')callee: a{ value: undefined, done: false }> callerObj.next('b')callee: b{ value: undefined, done: false }

throw() andreturn() are forwarded in a similar manner.

22.4.7.2The semantics ofyield* expressed in JavaScript

I’ll explain the complete semantics ofyield* by showing how you’d implemented it in JavaScript.

The following statement:

letyieldStarResult=yield*calleeFunc();

is roughly equivalent to:

letyieldStarResult;constcalleeObj=calleeFunc();letprevReceived=undefined;while(true){try{// Forward input previously receivedconst{value,done}=calleeObj.next(prevReceived);if(done){yieldStarResult=value;break;}prevReceived=yieldvalue;}catch(e){// Pretend `return` can be caught like an exceptionif(einstanceofReturn){// Forward input received via return()calleeObj.return(e.returnedValue);returne.returnedValue;// “re-throw”}else{// Forward input received via throw()calleeObj.throw(e);// may throw}}}

To keep things simple, several things are missing in this code:

22.5Generators as coroutines (cooperative multitasking)

We have seen generators being used as either sources or sinks of data. For many applications, it’s good practice to strictly separate these two roles, because it keeps things simpler. This section describes the full generator interface (which combines both roles) and one use case where both roles are needed: cooperative multitasking, where tasks must be able to both send and receive information.

22.5.1The full generator interface

The full interface of generator objects,Generator, handles both output and input:

interfaceGenerator{next(value?:any):IteratorResult;throw(value?:any):IteratorResult;return(value?:any):IteratorResult;}interfaceIteratorResult{value:any;done:boolean;}

This interface is described in the spec in the section “Properties of Generator Prototype”.

The interfaceGenerator combines two interfaces that we have seen previously:Iterator for output andObserver for input.

interfaceIterator{// data producernext():IteratorResult;return?(value?:any):IteratorResult;}interfaceObserver{// data consumernext(value?:any):void;return(value?:any):void;throw(error):void;}

22.5.2Cooperative multitasking

Cooperative multitasking is an application of generators where we need them to handle both output and input. Before we get into how that works, let’s first review the current state of parallelism in JavaScript.

JavaScript runs in a single process. There are two ways in which this limitation is being abolished:

Two use cases benefit from cooperative multitasking, because they involve control flows that are mostly sequential, anyway, with occasional pauses:

22.5.2.1Simplifying asynchronous computations via generators

Several Promise-based libraries simplify asynchronous code via generators. Generators are ideal as clients of Promises, because they can be suspended until a result arrives.

The following example demonstrates what that looks like if one usesthe libraryco by T.J. Holowaychuk. We need two libraries (if we run Node.js code viababel-node):

importfetchfrom'isomorphic-fetch';constco=require('co');

co is the actual library for cooperative multitasking,isomorphic-fetch is a polyfill for the new Promise-basedfetch API (a replacement ofXMLHttpRequest; read “That’s so fetch!” by Jake Archibald for more information).fetch makes it easy to write a functiongetFile that returns the text of a file at aurl via a Promise:

functiongetFile(url){returnfetch(url).then(request=>request.text());}

We now have all the ingredients to useco. The following task reads the texts of two files, parses the JSON inside them and logs the result.

co(function*(){try{const[croftStr,bondStr]=yieldPromise.all([// (A)getFile('http://localhost:8000/croft.json'),getFile('http://localhost:8000/bond.json'),]);constcroftJson=JSON.parse(croftStr);constbondJson=JSON.parse(bondStr);console.log(croftJson);console.log(bondJson);}catch(e){console.log('Failure to read: '+e);}});

Note how nicely synchronous this code looks, even though it makes an asynchronous call in line A. A generator-as-task makes an async call by yielding a Promise to the scheduler functionco. The yielding pauses the generator. Once the Promise returns a result, the scheduler resumes the generator by passing it the result vianext(). A simple version ofco looks as follows.

functionco(genFunc){constgenObj=genFunc();step(genObj.next());functionstep({value,done}){if(!done){// A Promise was yieldedvalue.then(result=>{step(genObj.next(result));// (A)}).catch(error=>{step(genObj.throw(error));// (B)});}}}

I have ignored thatnext() (line A) andthrow() (line B) may throw exceptions (whenever an exception escapes the body of the generator function).

22.5.3The limitations of cooperative multitasking via generators

Coroutines are cooperatively multitasked tasks that have no limitations: Inside a coroutine, any function can suspend the whole coroutine (the function activation itself, the activation of the function’s caller, the caller’s caller, etc.).

In contrast, you can only suspend a generator from directly within a generator and only the current function activation is suspended. Due to these limitations, generators are occasionally calledshallow coroutines [3].

22.5.3.1The benefits of the limitations of generators

The limitations of generators have two main benefits:

JavaScript already has a very simple style of cooperative multitasking: the event loop, which schedules the execution of tasks in a queue. Each task is started by calling a function and finished once that function is finished. Events,setTimeout() and other mechanisms add tasks to the queue.

This explanation of the event loop is a simplification that is good enough for now. If you are interested in details, consultthe chapter on asynchronous programming.

This style of multitasking makes one important guarantee:run to completion; every function can rely on not being interrupted by another task until it is finished. Functions become transactions and can perform complete algorithms without anyone seeing the data they operate on in an intermediate state. Concurrent access to shared data makes multitasking complicated and is not allowed by JavaScript’s concurrency model. That’s why run to completion is a good thing.

Alas, coroutines prevent run to completion, because any function could suspend its caller. For example, the following algorithm consists of multiple steps:

step1(sharedData);step2(sharedData);lastStep(sharedData);

Ifstep2 was to suspend the algorithm, other tasks could run before the last step of the algorithm is performed. Those tasks could contain other parts of the application which would seesharedData in an unfinished state. Generators preserve run to completion, they only suspend themselves and return to their caller.

co and similar libraries give you most of the power of coroutines, without their disadvantages:

22.6Examples of generators

This section gives several examples of what generators can be used for.

The following GitHub repository contains the example code:generator-examples

22.6.1Implementing iterables via generators

Inthe chapter on iteration, I implemented several iterables “by hand”. In this section, I use generators, instead.

22.6.1.1The iterable combinatortake()

take() converts a (potentially infinite) sequence of iterated values into a sequence of lengthn:

function*take(n,iterable){for(constxofiterable){if(n<=0)return;n--;yieldx;}}

The following is an example of using it:

constarr=['a','b','c','d'];for(constxoftake(2,arr)){console.log(x);}// Output:// a// b

An implementation oftake() without generators is more complicated:

functiontake(n,iterable){constiter=iterable[Symbol.iterator]();return{[Symbol.iterator](){returnthis;},next(){if(n>0){n--;returniter.next();}else{maybeCloseIterator(iter);return{done:true};}},return(){n=0;maybeCloseIterator(iter);}};}functionmaybeCloseIterator(iterator){if(typeofiterator.return==='function'){iterator.return();}}

Note thatthe iterable combinatorzip() does not profit much from being implemented via a generator, because multiple iterables are involved andfor-of can’t be used.

22.6.1.2Infinite iterables

naturalNumbers() returns an iterable over all natural numbers:

function*naturalNumbers(){for(letn=0;;n++){yieldn;}}

This function is often used in conjunction with a combinator:

for(constxoftake(3,naturalNumbers())){console.log(x);}// Output// 0// 1// 2

Here is the non-generator implementation, so you can compare:

functionnaturalNumbers(){letn=0;return{[Symbol.iterator](){returnthis;},next(){return{value:n++};}}}
22.6.1.3Array-inspired iterable combinators:map,filter

Arrays can be transformed via the methodsmap andfilter. Those methods can be generalized to have iterables as input and iterables as output.

22.6.1.3.1A generalizedmap()

This is the generalized version ofmap:

function*map(iterable,mapFunc){for(constxofiterable){yieldmapFunc(x);}}

map() works with infinite iterables:

> [...take(4, map(naturalNumbers(), x => x * x))][ 0, 1, 4, 9 ]
22.6.1.3.2A generalizedfilter()

This is the generalized version offilter:

function*filter(iterable,filterFunc){for(constxofiterable){if(filterFunc(x)){yieldx;}}}

filter() works with infinite iterables:

> [...take(4, filter(naturalNumbers(), x => (x % 2) === 0))][ 0, 2, 4, 6 ]

22.6.2Generators for lazy evaluation

The next two examples show how generators can be used to process a stream of characters.

The neat thing is that everything is computedlazily (incrementally and on demand): computation starts as soon as the first character arrives. For example, we don’t have to wait until we have all characters to get the first word.

22.6.2.1Lazy pull (generators as iterators)

Lazy pull with generators works as follows. The three generators implementing steps 1–3 are chained as follows:

addNumbers(extractNumbers(tokenize(CHARS)))

Each of the chain members pulls data from a source and yields a sequence of items. Processing starts withtokenize whose source is the stringCHARS.

22.6.2.1.1Step 1 – tokenizing

The following trick makes the code a bit simpler: the end-of-sequence iterator result (whose propertydone isfalse) is converted into the sentinel valueEND_OF_SEQUENCE.

/** * Returns an iterable that transforms the input sequence * of characters into an output sequence of words. */function*tokenize(chars){constiterator=chars[Symbol.iterator]();letch;do{ch=getNextItem(iterator);// (A)if(isWordChar(ch)){letword='';do{word+=ch;ch=getNextItem(iterator);// (B)}while(isWordChar(ch));yieldword;// (C)}// Ignore all other characters}while(ch!==END_OF_SEQUENCE);}constEND_OF_SEQUENCE=Symbol();functiongetNextItem(iterator){const{value,done}=iterator.next();returndone?END_OF_SEQUENCE:value;}functionisWordChar(ch){returntypeofch==='string'&&/^[A-Za-z0-9]$/.test(ch);}

How is this generator lazy? When you ask it for a token vianext(), it pulls itsiterator (lines A and B) as often as needed to produce as token and then yields that token (line C). Then it pauses until it is again asked for a token. That means that tokenization starts as soon as the first characters are available, which is convenient for streams.

Let’s try out tokenization. Note that the spaces and the dot are non-words. They are ignored, but they separate words. We use the fact that strings are iterables over characters (Unicode code points). The result oftokenize() is an iterable over words, which we turn into an array via the spread operator (...).

> [...tokenize('2 apples and 5 oranges.')][ '2', 'apples', 'and', '5', 'oranges' ]
22.6.2.1.2Step 2 – extracting numbers

This step is relatively simple, we onlyyield words that contain nothing but digits, after converting them to numbers viaNumber().

/** * Returns an iterable that filters the input sequence * of words and only yields those that are numbers. */function*extractNumbers(words){for(constwordofwords){if(/^[0-9]+$/.test(word)){yieldNumber(word);}}}

You can again see the laziness: If you ask for a number vianext(), you get one (viayield) as soon as one is encountered inwords.

Let’s extract the numbers from an Array of words:

> [...extractNumbers(['hello', '123', 'world', '45'])][ 123, 45 ]

Note that strings are converted to numbers.

22.6.2.1.3Step 3 – adding numbers
/** * Returns an iterable that contains, for each number in * `numbers`, the total sum of numbers encountered so far. * For example: 7, 4, -1 --> 7, 11, 10 */function*addNumbers(numbers){letresult=0;for(constnofnumbers){result+=n;yieldresult;}}

Let’s try a simple example:

>[...addNumbers([5,-2,12])][5,3,15]
22.6.2.1.4Pulling the output

On its own, the chain of generator doesn’t produce output. We need to actively pull the output via the spread operator:

constCHARS='2 apples and 5 oranges.';constCHAIN=addNumbers(extractNumbers(tokenize(CHARS)));console.log([...CHAIN]);// [ 2, 7 ]

The helper functionlogAndYield allows us to examine whether things are indeed computed lazily:

function*logAndYield(iterable,prefix=''){for(constitemofiterable){console.log(prefix+item);yielditem;}}constCHAIN2=logAndYield(addNumbers(extractNumbers(tokenize(logAndYield(CHA\RS)))),'-> ');[...CHAIN2];// Output:// 2//// -> 2// a// p// p// l// e// s//// a// n// d//// 5//// -> 7// o// r// a// n// g// e// s// .

The output shows thataddNumbers produces a result as soon as the characters'2' and' ' are received.

22.6.2.2Lazy push (generators as observables)

Not much work is needed to convert the previous pull-based algorithm into a push-based one. The steps are the same. But instead of finishing via pulling, we start via pushing.

As previously explained, if generators receive input viayield, the first invocation ofnext() on the generator object doesn’t do anything. That’s why I usethe previously shown helper functioncoroutine() to create coroutines here. It executes the firstnext() for us.

The following functionsend() does the pushing.

/** * Pushes the items of `iterable` into `sink`, a generator. * It uses the generator method `next()` to do so. */functionsend(iterable,sink){for(constxofiterable){sink.next(x);}sink.return();// signal end of stream}

When a generator processes a stream, it needs to be aware of the end of the stream, so that it can clean up properly. For pull, we did this via a special end-of-stream sentinel. For push, the end-of-stream is signaled viareturn().

Let’s testsend() via a generator that simply outputs everything it receives:

/** * This generator logs everything that it receives via `next()`. */constlogItems=coroutine(function*(){try{while(true){constitem=yield;// receive item via `next()`console.log(item);}}finally{console.log('DONE');}});

Let’s sendlogItems() three characters via a string (which is an iterable over Unicode code points).

> send('abc', logItems());abcDONE
22.6.2.2.1Step 1 – tokenize

Note how this generator reacts to the end of the stream (as signaled viareturn()) in twofinally clauses. We depend onreturn() being sent to either one of the twoyields. Otherwise, the generator would never terminate, because the infinite loop starting in line A would never terminate.

/** * Receives a sequence of characters (via the generator object * method `next()`), groups them into words and pushes them * into the generator `sink`. */consttokenize=coroutine(function*(sink){try{while(true){// (A)letch=yield;// (B)if(isWordChar(ch)){// A word has startedletword='';try{do{word+=ch;ch=yield;// (C)}while(isWordChar(ch));}finally{// The word is finished.// We get here if// - the loop terminates normally// - the loop is terminated via `return()` in line Csink.next(word);// (D)}}// Ignore all other characters}}finally{// We only get here if the infinite loop is terminated// via `return()` (in line B or C).// Forward `return()` to `sink` so that it is also// aware of the end of stream.sink.return();}});functionisWordChar(ch){return/^[A-Za-z0-9]$/.test(ch);}

This time, the laziness is driven by push: as soon as the generator has received enough characters for a word (in line C), it pushes the word intosink (line D). That is, the generator does not wait until it has received all characters.

tokenize() demonstrates that generators work well as implementations of linear state machines. In this case, the machine has two states: “inside a word” and “not inside a word”.

Let’s tokenize a string:

> send('2 apples and 5 oranges.', tokenize(logItems()));2applesand5oranges
22.6.2.2.2Step 2 – extract numbers

This step is straightforward.

/** * Receives a sequence of strings (via the generator object * method `next()`) and pushes only those strings to the generator * `sink` that are “numbers” (consist only of decimal digits). */constextractNumbers=coroutine(function*(sink){try{while(true){constword=yield;if(/^[0-9]+$/.test(word)){sink.next(Number(word));}}}finally{// Only reached via `return()`, forward.sink.return();}});

Things are again lazy: as soon as a number is encountered, it is pushed tosink.

Let’s extract the numbers from an Array of words:

> send(['hello', '123', 'world', '45'], extractNumbers(logItems()));12345DONE

Note that the input is a sequence of strings, while the output is a sequence of numbers.

22.6.2.2.3Step 3 – add numbers

This time, we react to the end of the stream by pushing a single value and then closing the sink.

/** * Receives a sequence of numbers (via the generator object * method `next()`). For each number, it pushes the total sum * so far to the generator `sink`. */constaddNumbers=coroutine(function*(sink){letsum=0;try{while(true){sum+=yield;sink.next(sum);}}finally{// We received an end-of-streamsink.return();// signal end of stream}});

Let’s try out this generator:

> send([5, -2, 12], addNumbers(logItems()));5315DONE
22.6.2.2.4Pushing the input

The chain of generators starts withtokenize and ends withlogItems, which logs everything it receives. We push a sequence of characters into the chain viasend:

constINPUT='2 apples and 5 oranges.';constCHAIN=tokenize(extractNumbers(addNumbers(logItems())));send(INPUT,CHAIN);// Output// 2// 7// DONE

The following code proves that processing really happens lazily:

constCHAIN2=tokenize(extractNumbers(addNumbers(logItems({prefix:'-> '})\)));send(INPUT,CHAIN2,{log:true});// Output// 2//// -> 2// a// p// p// l// e// s//// a// n// d//// 5//// -> 7// o// r// a// n// g// e// s// .// DONE

The output shows thataddNumbers produces a result as soon as the characters'2' and' ' are pushed.

22.6.3Cooperative multi-tasking via generators

22.6.3.1Pausing long-running tasks

In this example, we create a counter that is displayed on a web page. We improve an initial version until we have a cooperatively multitasked version that doesn’t block the main thread and the user interface.

This is the part of the web page in which the counter should be displayed:

<body>    Counter:<spanid="counter"></span></body>

This function displays a counter that counts up forever5:

functioncountUp(start=0){constcounterSpan=document.querySelector('#counter');while(true){counterSpan.textContent=String(start);start++;}}

If you ran this function, it would completely block the user interface thread in which it runs and its tab would become unresponsive.

Let’s implement the same functionality via a generator that periodically pauses viayield (a scheduling function for running this generator is shown later):

function*countUp(start=0){constcounterSpan=document.querySelector('#counter');while(true){counterSpan.textContent=String(start);start++;yield;// pause}}

Let’s add one small improvement. We move the update of the user interface to another generator,displayCounter, which we call viayield*. As it is a generator, it can also take care of pausing.

function*countUp(start=0){while(true){start++;yield*displayCounter(start);}}function*displayCounter(counter){constcounterSpan=document.querySelector('#counter');counterSpan.textContent=String(counter);yield;// pause}

Lastly, this is a scheduling function that we can use to runcountUp(). Each execution step of the generator is handled by a separate task, which is created viasetTimeout(). That means that the user interface can schedule other tasks in between and will remain responsive.

functionrun(generatorObject){if(!generatorObject.next().done){// Add a new task to the event queuesetTimeout(function(){run(generatorObject);},1000);}}

With the help ofrun, we get a (nearly) infinite count-up that doesn’t block the user interface:

run(countUp());

You canrun this example online.

22.6.3.2Cooperative multitasking with generators and Node.js-style callbacks

If you call a generator function (or method), it does not have access to its generator object; itsthis is thethis it would have if it were a non-generator function. A work-around is to pass the generator object into the generator function viayield.

The following Node.js script uses this technique, but wraps the generator object in a callback (next, line A). It must be run viababel-node.

import{readFile}from'fs';constfileNames=process.argv.slice(2);run(function*(){constnext=yield;for(constfoffileNames){constcontents=yieldreadFile(f,{encoding:'utf8'},next);console.log('##### '+f);console.log(contents);}});

In line A, we get a callback that we can use with functions that follow Node.js callback conventions. The callback uses the generator object to wake up the generator, as you can see in the implementation ofrun():

functionrun(generatorFunction){constgeneratorObject=generatorFunction();// Step 1: Proceed to first `yield`generatorObject.next();// Step 2: Pass in a function that the generator can use as a callbackfunctionnextFunction(error,result){if(error){generatorObject.throw(error);}else{generatorObject.next(result);}}generatorObject.next(nextFunction);// Subsequent invocations of `next()` are triggered by `nextFunction`}
22.6.3.3Communicating Sequential Processes (CSP)

The libraryjs-csp brings Communicating Sequential Processes (CSP) to JavaScript, a style of cooperative multitasking that is similar to ClojureScript’s core.async and Go’sgoroutines.js-csp has two abstractions:

As an example, let’s use CSP to handle DOM events, in a manner reminiscent of Functional Reactive Programming. The following code uses the functionlisten() (which is shown later) to create a channel that outputsmousemove events. It then continuously retrieves the output viatake, inside an infinite loop. Thanks toyield, the process blocks until the channel has output.

importcspfrom'js-csp';csp.go(function*(){constelement=document.querySelector('#uiElement1');constchannel=listen(element,'mousemove');while(true){constevent=yieldcsp.take(channel);constx=event.layerX||event.clientX;consty=event.layerY||event.clientY;element.textContent=`${x},${y}`;}});

listen() is implemented as follows.

functionlisten(element,type){constchannel=csp.chan();element.addEventListener(type,event=>{csp.putAsync(channel,event);});returnchannel;}

This example is taken from the blog post “Taming the Asynchronous Beast with CSP Channels in JavaScript” by James Long. Consult this blog post for more information on CSP.

22.7Inheritance within the iteration API (including generators)

This is a diagram of how various objects are connected in ECMAScript 6 (it is based onAllen Wirf-Brock’s diagram in the ECMAScript specification):

Legend:

The diagram reveals two interesting facts:

First, a generator functiong works very much like a constructor (however, you can’t invoke it vianew; that causes aTypeError): The generator objects it creates are instances of it, methods added tog.prototype become prototype methods, etc.:

>function*g(){}>g.prototype.hello=function(){return'hi!'};>constobj=g();>objinstanceofgtrue>obj.hello()'hi!'

Second, if you want to make methods available for all generator objects, it’s best to add them to(Generator).prototype. One way of accessing that object is as follows:

constGenerator=Object.getPrototypeOf(function*(){});Generator.prototype.hello=function(){return'hi!'};constgeneratorObject=(function*(){})();generatorObject.hello();// 'hi!'

22.7.1IteratorPrototype

There is no(Iterator) in the diagram, because no such object exists. But, given howinstanceof works and because(IteratorPrototype) is a prototype ofg1(), you could still say thatg1() is an instance ofIterator.

All iterators in ES6 have(IteratorPrototype) in their prototype chain. That object is iterable, because it has the following method. Therefore, all ES6 iterators are iterable (as a consequence, you can applyfor-of etc. to them).

[Symbol.iterator](){returnthis;}

The specification recommends to use the following code to access(IteratorPrototype):

constproto=Object.getPrototypeOf.bind(Object);constIteratorPrototype=proto(proto([][Symbol.iterator]()));

You could also use:

constIteratorPrototype=proto(proto(function*(){}.prototype));

Quoting the ECMAScript 6 specification:

ECMAScript code may also define objects that inherit fromIteratorPrototype. TheIteratorPrototype object provides a place where additional methods that are applicable to all iterator objects may be added.

IteratorPrototype will probably become directly accessible in an upcoming version of ECMAScript and contain tool methods such asmap() andfilter() (source).

22.7.2The value ofthis in generators

A generator function combines two concerns:

  1. It is a function that sets up and returns a generator object.
  2. It contains the code that the generator object steps through.

That’s why it’s not immediately obvious what the value ofthis should be inside a generator.

In function calls and method calls,this is what it would be ifgen() wasn’t a generator function, but a normal function:

function*gen(){'use strict';// just in caseyieldthis;}// Retrieve the yielded value via destructuringconst[functionThis]=gen();console.log(functionThis);// undefinedconstobj={method:gen};const[methodThis]=obj.method();console.log(methodThis===obj);// true

If you accessthis in a generator that was invoked vianew, you get aReferenceError (source: ES6 spec):

function*gen(){console.log(this);// ReferenceError}newgen();

A work-around is to wrap the generator in a normal function that hands the generator its generator object vianext(). That means that the generator must use its firstyield to retrieve its generator object:

constgeneratorObject=yield;

22.8Style consideration: whitespace before and after the asterisk

Reasonable – and legal – variations of formatting the asterisk are:

Let’s figure out which of these variations make sense for which constructs and why.

22.8.1Generator function declarations and expressions

Here, the star is only used becausegenerator (or something similar) isn’t available as a keyword. If it were, then a generator function declaration would look like this:

generatorfoo(x,y){···}

Instead ofgenerator, ECMAScript 6 marks thefunction keyword with an asterisk. Thus,function* can be seen as a synonym forgenerator, which suggests writing generator function declarations as follows.

function*foo(x,y){···}

Anonymous generator function expressions would be formatted like this:

constfoo=function*(x,y){···}

22.8.2Generator method definitions

When writing generator method definitions, I recommend to format the asterisk as follows.

constobj={*generatorMethod(x,y){···}};

There are three arguments in favor of writing a space after the asterisk.

First, the asterisk shouldn’t be part of the method name. On one hand, it isn’t part of the name of a generator function. On the other hand, the asterisk is only mentioned when defining a generator, not when using it.

Second, a generator method definition is an abbreviation for the following syntax. (To make my point, I’m redundantly giving the function expression a name, too.)

constobj={generatorMethod:function*generatorMethod(x,y){···}};

If method definitions are about omitting thefunction keyword then the asterisk should be followed by a space.

Third, generator method definitions are syntactically similar to getters and setters (which are already available in ECMAScript 5):

constobj={getfoo(){···}setfoo(value){···}};

The keywordsget andset can be seen as modifiers of a normal method definition. Arguably, an asterisk is also such a modifier.

22.8.3Formatting recursiveyield

The following is an example of a generator function yielding its own yielded values recursively:

function*foo(x){···yield*foo(x-1);···}

The asterisk marks a different kind ofyield operator, which is why the above way of writing it makes sense.

22.8.4Documenting generator functions and methods

Kyle Simpson (@getify) proposed something interesting: Given that we often append parentheses when we write about functions and methods such asMath.max(), wouldn’t it make sense to prepend an asterisk when writing about generator functions and methods? For example: should we write*foo() to refer to the generator function in the previous subsection? Let me argue against that.

When it comes to writing a function that returns an iterable, a generator is only one of the several options. I think it is better to not give away this implementation detail via marked function names.

Furthermore, you don’t use the asterisk when calling a generator function, but you do use parentheses.

Lastly, the asterisk doesn’t provide useful information –yield* can also be used with functions that return an iterable. But it may make sense to mark the names of functions and methods (including generators) that return iterables. For example, via the suffixIter.

22.9FAQ: generators

22.9.1Why use the keywordfunction* for generators and notgenerator?

Due to backward compatibility, using the keywordgenerator wasn’t an option. For example, the following code (a hypothetical ES6 anonymous generator expression) could be an ES5 function call followed by a code block.

generator(a,b,c){···}

I find that the asterisk naming scheme extends nicely toyield*.

22.9.2Isyield a keyword?

yield is only a reserved word in strict mode. A trick is used to bring it to ES6 sloppy mode: it becomes acontextual keyword, one that is only available inside generators.

22.10Conclusion

I hope that this chapter convinced you that generators are a useful and versatile tool.

I like that generators let you implement cooperatively multitasked tasks that block while making asynchronous function calls. In my opinion that’s the right mental model for async calls. Hopefully, JavaScript goes further in this direction in the future.

22.11Further reading

Sources of this chapter:

[1] “Async Generator Proposal” by Jafar Husain

[2] “A Curious Course on Coroutines and Concurrency” by David Beazley

[3] “Why coroutines won’t work on the web” by David Herman

Next:V Standard library

[8]ページ先頭

©2009-2025 Movatter.jp