Thunks

So far, we’ve made the assumption that yielding a Promise from a generator — and having that Promise resume the generator via a helper utility like run(..) — was the best possible way to manage asynchrony with generators. To be clear, it is.

But we skipped over another pattern that has some mildly widespread adoption, so in the interest of completeness we’ll take a brief look at it.

In general computer science, there’s an old pre-JS concept called a “thunk.” Without getting bogged down in the historical nature, a narrow expression of a thunk in JS is a function that — without any parameters — is wired to call another function.

In other words, you wrap a function definition around function call — with any parameters it needs — to defer the execution of that call, and that wrapping function is a thunk. When you later execute the thunk, you end up calling the original function.

For example:

  1. function foo(x,y) {
  2. return x + y;
  3. }
  4. function fooThunk() {
  5. return foo( 3, 4 );
  6. }
  7. // later
  8. console.log( fooThunk() ); // 7

So, a synchronous thunk is pretty straightforward. But what about an async thunk? We can essentially extend the narrow thunk definition to include it receiving a callback.

Consider:

  1. function foo(x,y,cb) {
  2. setTimeout( function(){
  3. cb( x + y );
  4. }, 1000 );
  5. }
  6. function fooThunk(cb) {
  7. foo( 3, 4, cb );
  8. }
  9. // later
  10. fooThunk( function(sum){
  11. console.log( sum ); // 7
  12. } );

As you can see, fooThunk(..) only expects a cb(..) parameter, as it already has values 3 and 4 (for x and y, respectively) pre-specified and ready to pass to foo(..). A thunk is just waiting around patiently for the last piece it needs to do its job: the callback.

You don’t want to make thunks manually, though. So, let’s invent a utility that does this wrapping for us.

Consider:

  1. function thunkify(fn) {
  2. var args = [].slice.call( arguments, 1 );
  3. return function(cb) {
  4. args.push( cb );
  5. return fn.apply( null, args );
  6. };
  7. }
  8. var fooThunk = thunkify( foo, 3, 4 );
  9. // later
  10. fooThunk( function(sum) {
  11. console.log( sum ); // 7
  12. } );

Tip: Here we assume that the original (foo(..)) function signature expects its callback in the last position, with any other parameters coming before it. This is a pretty ubiquitous “standard” for async JS function standards. You might call it “callback-last style.” If for some reason you had a need to handle “callback-first style” signatures, you would just make a utility that used args.unshift(..) instead of args.push(..).

The preceding formulation of thunkify(..) takes both the foo(..) function reference, and any parameters it needs, and returns back the thunk itself (fooThunk(..)). However, that’s not the typical approach you’ll find to thunks in JS.

Instead of thunkify(..) making the thunk itself, typically — if not perplexingly — the thunkify(..) utility would produce a function that produces thunks.

Uhhhh… yeah.

Consider:

  1. function thunkify(fn) {
  2. return function() {
  3. var args = [].slice.call( arguments );
  4. return function(cb) {
  5. args.push( cb );
  6. return fn.apply( null, args );
  7. };
  8. };
  9. }

The main difference here is the extra return function() { .. } layer. Here’s how its usage differs:

  1. var whatIsThis = thunkify( foo );
  2. var fooThunk = whatIsThis( 3, 4 );
  3. // later
  4. fooThunk( function(sum) {
  5. console.log( sum ); // 7
  6. } );

Obviously, the big question this snippet implies is what is whatIsThis properly called? It’s not the thunk, it’s the thing that will produce thunks from foo(..) calls. It’s kind of like a “factory” for “thunks.” There doesn’t seem to be any kind of standard agreement for naming such a thing.

So, my proposal is “thunkory” (“thunk” + “factory”). So, thunkify(..) produces a thunkory, and a thunkory produces thunks. That reasoning is symmetric to my proposal for “promisory” in Chapter 3:

  1. var fooThunkory = thunkify( foo );
  2. var fooThunk1 = fooThunkory( 3, 4 );
  3. var fooThunk2 = fooThunkory( 5, 6 );
  4. // later
  5. fooThunk1( function(sum) {
  6. console.log( sum ); // 7
  7. } );
  8. fooThunk2( function(sum) {
  9. console.log( sum ); // 11
  10. } );

Note: The running foo(..) example expects a style of callback that’s not “error-first style.” Of course, “error-first style” is much more common. If foo(..) had some sort of legitimate error-producing expectation, we could change it to expect and use an error-first callback. None of the subsequent thunkify(..) machinery cares what style of callback is assumed. The only difference in usage would be fooThunk1(function(err,sum){...

Exposing the thunkory method — instead of how the earlier thunkify(..) hides this intermediary step — may seem like unnecessary complication. But in general, it’s quite useful to make thunkories at the beginning of your program to wrap existing API methods, and then be able to pass around and call those thunkories when you need thunks. The two distinct steps preserve a cleaner separation of capability.

To illustrate:

  1. // cleaner:
  2. var fooThunkory = thunkify( foo );
  3. var fooThunk1 = fooThunkory( 3, 4 );
  4. var fooThunk2 = fooThunkory( 5, 6 );
  5. // instead of:
  6. var fooThunk1 = thunkify( foo, 3, 4 );
  7. var fooThunk2 = thunkify( foo, 5, 6 );

Regardless of whether you like to deal with the thunkories explicitly or not, the usage of thunks fooThunk1(..) and fooThunk2(..) remains the same.

s/promise/thunk/

So what’s all this thunk stuff have to do with generators?

Comparing thunks to promises generally: they’re not directly interchangable as they’re not equivalent in behavior. Promises are vastly more capable and trustable than bare thunks.

But in another sense, they both can be seen as a request for a value, which may be async in its answering.

Recall from Chapter 3 we defined a utility for promisifying a function, which we called Promise.wrap(..) — we could have called it promisify(..), too! This Promise-wrapping utility doesn’t produce Promises; it produces promisories that in turn produce Promises. This is completely symmetric to the thunkories and thunks presently being discussed.

To illustrate the symmetry, let’s first alter the running foo(..) example from earlier to assume an “error-first style” callback:

  1. function foo(x,y,cb) {
  2. setTimeout( function(){
  3. // assume `cb(..)` as "error-first style"
  4. cb( null, x + y );
  5. }, 1000 );
  6. }

Now, we’ll compare using thunkify(..) and promisify(..) (aka Promise.wrap(..) from Chapter 3):

  1. // symmetrical: constructing the question asker
  2. var fooThunkory = thunkify( foo );
  3. var fooPromisory = promisify( foo );
  4. // symmetrical: asking the question
  5. var fooThunk = fooThunkory( 3, 4 );
  6. var fooPromise = fooPromisory( 3, 4 );
  7. // get the thunk answer
  8. fooThunk( function(err,sum){
  9. if (err) {
  10. console.error( err );
  11. }
  12. else {
  13. console.log( sum ); // 7
  14. }
  15. } );
  16. // get the promise answer
  17. fooPromise
  18. .then(
  19. function(sum){
  20. console.log( sum ); // 7
  21. },
  22. function(err){
  23. console.error( err );
  24. }
  25. );

Both the thunkory and the promisory are essentially asking a question (for a value), and respectively the thunk fooThunk and promise fooPromise represent the future answers to that question. Presented in that light, the symmetry is clear.

With that perspective in mind, we can see that generators which yield Promises for asynchrony could instead yield thunks for asynchrony. All we’d need is a smarter run(..) utility (like from before) that can not only look for and wire up to a yielded Promise but also to provide a callback to a yielded thunk.

Consider:

  1. function *foo() {
  2. var val = yield request( "http://some.url.1" );
  3. console.log( val );
  4. }
  5. run( foo );

In this example, request(..) could either be a promisory that returns a promise, or a thunkory that returns a thunk. From the perspective of what’s going on inside the generator code logic, we don’t care about that implementation detail, which is quite powerful!

So, request(..) could be either:

  1. // promisory `request(..)` (see Chapter 3)
  2. var request = Promise.wrap( ajax );
  3. // vs.
  4. // thunkory `request(..)`
  5. var request = thunkify( ajax );

Finally, as a thunk-aware patch to our earlier run(..) utility, we would need logic like this:

  1. // ..
  2. // did we receive a thunk back?
  3. else if (typeof next.value == "function") {
  4. return new Promise( function(resolve,reject){
  5. // call the thunk with an error-first callback
  6. next.value( function(err,msg) {
  7. if (err) {
  8. reject( err );
  9. }
  10. else {
  11. resolve( msg );
  12. }
  13. } );
  14. } )
  15. .then(
  16. handleNext,
  17. function handleErr(err) {
  18. return Promise.resolve(
  19. it.throw( err )
  20. )
  21. .then( handleResult );
  22. }
  23. );
  24. }

Now, our generators can either call promisories to yield Promises, or call thunkories to yield thunks, and in either case, run(..) would handle that value and use it to wait for the completion to resume the generator.

Symmetry wise, these two approaches look identical. However, we should point out that’s true only from the perspective of Promises or thunks representing the future value continuation of a generator.

From the larger perspective, thunks do not in and of themselves have hardly any of the trustability or composability guarantees that Promises are designed with. Using a thunk as a stand-in for a Promise in this particular generator asynchrony pattern is workable but should be seen as less than ideal when compared to all the benefits that Promises offer (see Chapter 3).

If you have the option, prefer yield pr rather than yield th. But there’s nothing wrong with having a run(..) utility which can handle both value types.

Note: The runner(..) utility in my asynquence library, which will be discussed in Appendix A, handles yields of Promises, thunks and asynquence sequences.