📜 ⬆️ ⬇️

Exploring JavaScript Generators



When I started writing on node.js, I hated two things: all the popular template engines and a huge number of callbacks. I voluntarily used callbacks because I understood all the power of event-oriented servers, but since then generators have appeared in JavaScript, and I look forward to the day when they will be implemented.

And this day comes. Today, generators are available in V8 and SpiderMonkey, implementation follows specification updates - this is the dawn of a new era!
')
As long as the V8 hides behind the command line flag, new Harmony features, such as generators, will take some time. Before they are available in all browsers, we can go ahead and learn to write asynchronous code with generators. Let's try these approaches early on.

You can use them today by downloading the unstable version of node 0.11 , which will be the next stable version. When running node, pass the --harmony or --harmony-generators flag.

So how do you use generators to rescue from callback hell? Generator functions can pause execution with the help of the yield operator, and transfer the result in or out when they are resumed or suspended. In this way, we can make a “pause” when the function waits for the result of another function without passing a callback into it.

Isn't it fun when I try to explain language constructs in our language? How about diving into the code?

Generator basics


Let's look at a primitive generator before we dive into an asynchronous world. Generators are declared as function* expressions:

 function* foo(x) { yield x + 1; var y = yield null; return x + y; } 

The following is an example of a call:

 var gen = foo(5); gen.next(); // { value: 6, done: false } gen.next(); // { value: null, done: false } gen.send(8); // { value: 13, done: true } 

If I took notes in class, I would write:


Asynchronous Solution # 1: Suspend


What to do with the code in which callback hell? Well, if we can arbitrarily suspend the execution of a function. We can turn our asynchronous callback code back into a synchronous-looking code with sugar crumbs.

Question: what is sugar?

The first solution is suggested in the suspend library. It is very simple. Only 16 lines of code , seriously.

This is how our code with this library looks like:

 var suspend = require('suspend'), fs = require('fs'); suspend(function*(resume) { var data = yield fs.readFile(__filename, 'utf8', resume); if(data[0]) { throw data[0]; } console.log(data[1]); })(); 

The suspend function passes your generator inside a normal function that starts the generator. It passes the resume function to the generator, the resume function must be used as a callback for all asynchronous calls, it resumes the generator with arguments containing the error and value flags.

Dance resume and generator are interesting, but there are some drawbacks. First, the backward array of two elements is inconvenient, even with destructuring ( var [err, res] = yield foo(resume) ). I would like to return only the value, and throw the error as an exception, if any. The library actually supports this, but as an option, I think this should be the default.

Secondly, it is inconvenient to always explicitly transfer resume, moreover, it is not suitable when you wait until the function above completes. And I still have to add a callback and call it at the end of the function, as is usually done in the node.

Finally, you cannot use more complex execution threads, for example with multiple concurrent calls. README claims that other execution flow control libraries are already solving this problem, and you should use suspend along with one of them, but I would prefer to see a flow control library that includes generator support.

Update from the author: kriskowal suggested this gist written by creationix , an improved stand-alone generator handler for callback-based code is implemented there. It is very cool to throw errors by default.

Asynchronous solution number 2: Promises


A more interesting way to control asynchronous execution flow is to use promises . A promise is an object that represents a future value, and you can provide promises to the calling flow of a program that represents asynchronous behavior.

I will not explain the promises here, since it will take too long and, moreover, there is already a good explanation . Recently, emphasis has been placed on defining behavior and API promises for interaction between libraries, but the idea is rather simple.

I am going to use the Q library for promises, because it already has preliminary support from generators, and is also quite mature. task.js was an early implementation of this idea, but it had a non-standard promises implementation.

Let's take a step back and look at a real life example. We too often use simple examples. This code creates a message, then gets it back, and receives a message with the same tags ( client is an instance of redis):

 client.hmset('blog::post', { date: '20130605', title: 'g3n3rat0rs r0ck', tags: 'js,node' }, function(err, res) { if(err) throw err; client.hgetall('blog::post', function(err, post) { if(err) throw err; var tags = post.tags.split(','); var posts = []; tags.forEach(function(tag) { client.hgetall('post::tag::' + tag, function(err, taggedPost) { if(err) throw err; posts.push(taggedPost); if(posts.length == tags.length) { //  -  post  taggedPosts client.quit(); } }); }); }); }); 

See how this example is ugly! Callbacks quickly press the code to the right side of our screen. In addition, to request all tags, we must manually manage each request and check when they are all ready.

Let's bring this code to Q promises.

 var db = { get: Q.nbind(client.get, client), set: Q.nbind(client.set, client), hmset: Q.nbind(client.hmset, client), hgetall: Q.nbind(client.hgetall, client) }; db.hmset('blog::post', { date: '20130605', title: 'g3n3rat0rs r0ck', tags: 'js,node' }).then(function() { return db.hgetall('blog::post'); }).then(function(post) { var tags = post.tags.split(','); return Q.all(tags.map(function(tag) { return db.hgetall('blog::tag::' + tag); })).then(function(taggedPosts) { //  -  post  taggedPosts client.quit(); }); }).done(); 

We had to wrap the redis functions, and thus turned the callback-based into promise-based, it's simple. As soon as we receive promises, you call then and wait for the result of the asynchronous operations. Much more detail is explained in the promises / A + specification .

Q implements several additional methods, such as all , it takes an array of promises and waits until each one of them completes. In addition, there is done , which says that your asynchronous process has ended and any unhandled errors should be thrown. According to the promises / A + specification, all exceptions should be converted to errors and passed to the error handler. Thus, you can be sure that all errors will be thrown if they do not have a handler. (If something is not clear, please read this article from Dominic.)

Notice how deep the final promise is. This is because we first need access to the post , and then to taggedPosts . It feels callback-style code, it's annoying.

And now is the time to evaluate the power of generators:

 Q.async(function*() { yield db.hmset('blog::post', { date: '20130605', title: 'g3n3rat0rs r0ck', tags: 'js,node' }); var post = yield db.hgetall('blog::post'); var tags = post.tags.split(','); var taggedPosts = yield Q.all(tags.map(function(tag) { return db.hgetall('blog::tag::' + tag); })); //  -  post  taggedPosts client.quit(); })().done(); 

Isn't that amazing? How does this actually happen?

Q.async takes a generator and returns a function that controls it, like the suspend library. However, the key difference here is that the generator gives (yields) promises. Q accepts each promise and connects the generator with it, does resume when the promise is completed, and sends the result back.

We should not control the awkward function resume - promises to handle it completely, and we get the advantage of promises .

One of the advantages is that we can use different Q promises when necessary, for example Q.all , which runs several asynchronous operations in parallel. Thus, you can easily combine similar Q promises and implicit promises in generators to create complex execution threads that look very clean.

Also note that we have no nesting problem at all. Since post and taggedPosts remain in the same scope, we no longer care about breaking the chain of scope into then , which is incredibly pleasing.

Error handling is very tricky, and you really need to understand how promises work before using them in generators. Errors and exceptions in promises are always passed to the error handling function, and never throw exceptions.

Any async generator is a promise, with no exceptions. You can manage errors using error callback: someGenerator().then(null, function(err) { ... }) .

However, there is a special behavior of generator promises, which is that any errors from promises thrown into the generator using the special method gen.throw will be thrown by an exception from the point where the generator was suspended. This means that you can use try/catch to handle errors in the generator:

 Q.async(function*() { try { var post = yield db.hgetall('blog::post'); var tags = post.tags.split(','); var taggedPosts = yield Q.all(tags.map(function(tag) { return db.hgetall('blog::tag::' + tag); })); //  -  post  taggedPosts } catch(e) { console.log(e); } client.quit(); })(); 

This works exactly as you expect: errors from any db.hgetall call will be handled in the catch handler, even if it is an error in a deep promise inside Q.all . Without try/catch exception will be passed to the caller's promise error handler (if there is no caller, the error will be suppressed).

Think about it - we can set exception handlers using try / catch for asynchronous code. The dynamic scope of the error handler will be correct; any unhandled errors that occur while the try block is being executed will be passed to catch . You can use finally to create confident “cleanup” code at startup even for an error, without the presence of an error handler.

In addition, use done whenever you use promises — by default, you can get thrown errors instead of quiet ignoring, which happens too often with asynchronous code. The way to use Q.async , as a rule, looks like this:

 var getTaggedPosts = Q.async(function*() { var post = yield db.hgetall('blog::post'); var tags = post.tags.split(','); return Q.all(tags.map(function(tag) { return db.hget('blog::tag::' + tag); })); }); 

The above is the library code that simply creates promises and does not handle errors. You call it like this:

 Q.async(function*() { var tagged = yield getTaggedPosts(); //  -   tagged })().done(); 

This is the top level code. As stated earlier, the done method is guaranteed to throw an error for any unhandled error as an exception. I believe that this approach is common, but you need to call an extra method. getTaggedPosts will be used by promise-generating functions. The code above is simply a top level code that is filled with promises.

I suggested Q.spawn in the pull request , and these changes already hit Q! This allows you to do a simple run of code that uses promises, even easier:

 Q.spawn(function*() { var tagged = yield getTaggedPosts(); //  -   tagged }); 

spawn takes a generator, immediately starts it, and automatically forwards all unprocessed errors. This is exactly equivalent to Q.done(Q.async(function*() { ... })()) .

Other approaches


Our promised-based generator code begins to take shape. Together with grains of sugar, we can remove a lot of excess baggage associated with asynchronous workflow.

After some time working with generators, I outlined several approaches.

Not worth it

If you have a short function that needs to wait only one promise, it is not worth creating a generator.

 var getKey = Q.async(function*(key) { var x = yield r.get(dbkey(key)); return x && parseInt(x, 10); }); 

Use this code:

 function getKey(key) { return r.get(dbkey(key)).then(function(x) { return x && parseInt(x, 10); }); } 

I think the latest version looks cleaner.

spawnMap

This is what I did often:

 yield Q.all(keys.map(Q.async(function*(dateKey) { var date = yield lookupDate(dateKey); obj[date] = yield getPosts(date); }))); 

It may be useful to have a spawnMap that performs Q.all(arr.map(Q.async(...))) for you.

 yield spawnMap(keys, function*(dateKey) { var date = yield lookupDate(dateKey); obj[date] = yield getPosts(date); }))); 

This is similar to the map method from the async library.

asyncCallback

The last thing I noticed: there are times when I want to create a Q.async function and make it forward all errors. This happens with normal callbacks from different libraries, such as express: app.get('/url', function() { ... }) .

I cannot convert the above callback to the Q.async function, because then all the errors will be quietly suppressed, I also cannot use the Q.spawn because it is not executed immediately. Perhaps something like asyncCallback would be good:

 function asyncCallback(gen) { return function() { return Q.async(gen).apply(null, arguments).done(); }; } app.get('/project/:name', asyncCallback(function*(req, res) { var counts = yield db.getCounts(req.params.name); var post = yield db.recentPost(); res.render('project.html', { counts: counts, post: post }); })); 

As a summary


When I researched the generators, I really hoped that they would help with asynchronous code. And, as it turned out, they really do it, although you need to understand how promises work in order to effectively combine them with generators. Creating promises makes implicit even more implicit, so I would not recommend you use async or spawn until you understand the whole promise.

Now we have a concise and incredibly powerful way to encode asynchronous behavior and we can use it for something more than just doing operations to work with the FS more beautiful. In fact, we have a great way to write short, distributed code that can run on different processors, or even machines, while remaining synchronous.

Update from the author: read my next article, A look at generators without Promise .

Source: https://habr.com/ru/post/182620/


All Articles