I don’t know about you, but I’m always amazed every time I see some clever usage of generator functions (the yield keyword). From things like speeding up your UI, to implementing thread pools, or computing prime numbers and infinite sequences, they’re always interesting. Last week, Shawn showed me a cool async pattern using generators which I really liked so I decided to blog about it.
Let’s start with the problem: I was writing some set of tests which were highly dependent on triggering actions and waiting events propagation to continue. This usually involves a lot of callbacks from short timeouts or event listeners, and you end up with code like this: [semi-pseudocode]
function goOn() { doThing3(); setTimeout(finishUp, 10); } function test() { doThing1(); setTimeout(function() { addEventListener("interesting-event", goOn, false); doThing2(); }, 10); }
Which is the modern day equivalent of spreading goto
s everywhere on your code (“oh, the horror!”), and makes it very hard to follow the intended logic, specially if you didn’t aptly name your functions with numbers to help you out.
To improve that, we can avoid nesting all of the calls and write a sequential function, and use the yield keyword to exit the function at one point and then continue running it from that point on. It’s like a set of sleep/wakeUp calls, and all the wakeUp control won’t be so mixed with our logic. So we would write as follows:
function wakeUp() { testGen.next(); } function test() { doThing1(); setTimeout(wakeUp, 10); yield; doThing2(); addEventListener("interesting-event", wakeUp, false); yield; doThing3(); setTimeout(wakeUp, 10); yield; finishUp(); }
Cleaner, isn’t it? Oh, and just a note, when you use this make sure you don’t end up subtly changing the logic of your calls, unlike “a friend of mine” who did this and broke the tests he had written. 🙂
Neat trick. Using generators rather than closures makes waiting-logic much easier to follow.
It’s also important to use events rather than timeouts. My experience is that almost every test that uses a timeout to “wait for something” is intermittently orange. http://joblivious.wordpress.com/2009/02/20/handling-intermittence-how-to-survive-test-driven-development/ explains this issue well.
If you specifically want to return to the event loop but not wait any longer, a 0ms setTimeout can work. But browsers tend to clamp setTimeout, so you need something like http://dbaron.org/log/20100309-faster-timeouts to be sure.
By: Jesse Ruderman on March 21, 2010
at 3:35 am
The trick of the zero timeout using postMessage is really cool. One thing I was wondering: if we exit the function with the specific goal of returning to the event loop, is it guaranteed that all the current pending events will be processed before the function being called again? Or does the 10~20ms clamping ended up helping us here?
If it is guaranteed, I think it’d be really useful to have that ZeroTimeout function available in some helper file of the test harness. (EventUtils?)
By: felipe on March 21, 2010
at 4:23 am
I think that’s how it works — the events are treated as a queue, and you’re just posting one to the end of the queue.
By: Jesse Ruderman on March 21, 2010
at 4:52 am
SimpleTest.executeSoon is that helper function.
By: Markus Stange on March 21, 2010
at 6:52 am
Great to know! then SimpleTest.executeSoon(wakeUp) it is
By: felipe on March 21, 2010
at 11:20 pm
Just make sure you don’t return early. 😉
By: Siddharth Agarwal on March 21, 2010
at 9:03 am
Oh, and it’s really good to see old CS concepts come back into fashion. I think coroutines are a very clean way to implement asynchronous logic, and that one of the worst effects of C being dominant for so many years was that coroutines fell into disuse.
Interestingly, Simon Tatham (of PuTTY fame) has a hacked-up implementation of coroutines in C: http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html
By: Siddharth Agarwal on March 21, 2010
at 9:30 am
That was a very cool read, thanks Sid.
Another cool project related to this is node.js (nodejs.org). Have you seen it? It’s a JS library that makes all File I/O through events, even calls which are blocking in the underlying filesystem.
By: felipe on March 21, 2010
at 11:32 pm
[…] Had some fun with using yield for async tests […]
By: Status Update: AppTabs, Extension Manager UI « Blair’s Brain on May 31, 2010
at 2:17 am