Thoughts On Defining Coroutines As Class Methods In Node.js And TypeScript
In the last couple of days, I've been digging into JavaScript Generators and Coroutines, specifically around how the execution context of a generator function can be configured. And, now that it's clear to me about how the "this" reference can be applied to a Generator Object, it's got me thinking about using Coroutines as class methods. Currently, a Coroutine is created by wrapping the execution of a Generator; but, this creates a complication in ES6 where the current Class semantics don't allow for "properties" to be part of the class definition. So, how can a computed function be used as a Class method?
If we were still using the ES5 class semantics, which is to say, directly using the underlying Prototype chain, this wouldn't be an issue since the Prototype of an Object is just another Object; and, there's nothing wrong with assigning a calculated method to an Object property:
// Require the core node modules.
var bluebird = require( "bluebird" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// Define the class constructor.
function Thing( label ) {
this.label = label;
}
// Define the class methods using the ES5 syntax (ie, the underlying Prototype chain).
Thing.prototype = {
// With the native prototype syntax, we can assign anything we want to the prototype,
// including, but not limited to, calculated functions like coroutines.
doAsync: bluebird.coroutine(
function* () {
var value = yield Promise.resolve( this.label );
return( value );
}
)
};
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
var thing = new Thing( "woot" );
thing
.doAsync()
.then(
function handleResolve( value ) {
console.log( "Do Async:", value );
}
)
;
Here, you can see that we're using Bluebird's coroutine() method to define the doAsync() method on the Prototype of the Thing "class". This means that only one instance of this doAsync() method - and the Coroutine - will ever be allocated, no matter how many instances of the Thing class are new'ed.
And, just for reference, here is what the Bluebird coroutine() method is doing, copied directly from the source code:
Promise.coroutine = function (generatorFunction, options) {
//Throw synchronously because Promise.coroutine is semantically
//something you call at "compile time" to annotate static functions
if (typeof generatorFunction !== "function") {
throw new TypeError(NOT_GENERATOR_ERROR);
}
var yieldHandler = Object(options).yieldHandler;
var PromiseSpawn$ = PromiseSpawn;
var stack = new Error().stack;
return function () {
var generator = generatorFunction.apply(this, arguments);
var spawn = new PromiseSpawn$(undefined, undefined, yieldHandler,
stack);
var ret = spawn.promise();
spawn._generator = generator;
spawn._promiseFulfilled(undefined);
return ret;
};
};
As an aside, though, relating back to my previous post, notice that Bluebird invokes the passed-in generator function using .apply(this). Doing so means that the generator function will execute in the same context as the coroutine. So, if the coroutine is invoked as a method of a class, the generator function will also execute in the context of said class.
But, getting back to coroutines, class methods, and ES6, we can't assign properties as part of the class definition. That said, we can mix ES6 and ES5 semantics, combining both "Classes" and "Prototypes":
// Require the core node modules.
var bluebird = require( "bluebird" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// Define the Thing class.
class Thing {
// I initialize the Thing class.
constructor( label ) {
this.label = label;
}
}
// In addition to any class methods defined as part of the class definition we can
// also add methods directly to the prototype using the ES5 semantics of the Prototype
// chain (since it's really all just the same thing under the hood, more or less).
Thing.prototype.doAsync = bluebird.coroutine(
function* () {
var value = yield Promise.resolve( this.label );
return( value );
}
);
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
var thing = new Thing( "woot 2" );
thing
.doAsync()
.then(
function handleResolve( value ) {
console.log( "Do Async:", value );
}
)
;
Here, you can see that we're mixing the ES6 Class semantics with the core Prototype semantics for Object creation. This works, in that only one instance of the Coroutine is ever defined; but, I think this violates the "Principle of Least Astonishment" by spreading the class definition across two distinct portions of a Module. If a developer were to open the module in her IDE, it may not be obvious that the Coroutine is part of the class since it is not part of the initial list of methods. Therefore, it may be "astonishing" to find that such a method even exists on the instantiated class at all.
To keep the Coroutine inline with the rest of the class definition, we can try creating a class method that encapsulates the creation of the Coroutine:
// Require the core node modules.
var bluebird = require( "bluebird" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// Define the Thing class.
class Thing {
// I initialize the Thing class.
constructor( label ) {
this.label = label;
}
// This time, we'll create a normal class method, which will exist on the prototype;
// but, the class method will do nothing more than create and invoke a Coroutine.
doAsync() {
var coroutine = bluebird.coroutine(
function* () {
var value = yield Promise.resolve( this.label );
return( value );
}
);
return( coroutine.call( this ) );
}
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
var thing = new Thing( "woot 3" );
thing
.doAsync()
.then(
function handleResolve( value ) {
console.log( "Do Async:", value );
}
)
;
Here, we're defining the doAsync() method on the Class prototype (using the Class semantics), which means that only one instance of the doAsync() method will ever be defined. But, it doesn't really matter because the doAsync() isn't doing any work directly. Instead, it allocates and consumes both a new Coroutine and a new Generator function on each invocation. This works, but seems somehow antithetical to the concept of Prototypal inheritance.
If we are concerned about the re-definition of methods, we could try to define the Coroutine in the constructor:
// Require the core node modules.
var bluebird = require( "bluebird" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// Define the Thing class.
class Thing {
// I initialize the Thing class.
constructor( label ) {
this.label = label;
// By defining the Coroutine in the constructor, we only allocate one instance
// of the Coroutine per instantiated object.
this.doAsync = bluebird.coroutine(
function* () {
var value = yield Promise.resolve( this.label );
return( value );
}
);
}
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
var thing = new Thing( "woot 4" );
thing
.doAsync()
.then(
function handleResolve( value ) {
console.log( "Do Async:", value );
}
)
;
Using this approach, we don't quite get the benefit of Prototypal inheritance; but, we still only define one Coroutine per instance of the parent Class. That said, I feel like this also violates the "Principle of Least Astonishment" as it, once again, spreads the class definition across two different portions of the module. Plus, it just looks wonky - I don't want my constructor to be doing so much "stuff" - I just want it to initialize the class properties.
Ironically, I thought this was one place where TypeScript actually shines. Meaning, in TypeScript, you can define "class methods" using calculated values:
// Require the core node modules.
var bluebird = require( "bluebird" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// Define the Thing class.
class Thing {
public label: string;
// I initialize the Thing class.
constructor( label: string ) {
this.label = label;
}
// In TypeScript, you can define calculated property methods as part of the Class
// definition. As such, we can keep Coroutine definitions inline with the rest of
// the class methods.
doAsync = bluebird.coroutine(
function* () {
var value: string = yield Promise.resolve( this.label );
return( value );
}
)
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
var thing = new Thing( "woot 5" );
thing
.doAsync()
.then(
function handleResolve( value: string ) {
console.log( "[TypeScript] Do Async:", value );
}
)
;
Here, you can see that the Coroutine is being defined inline with the rest of the class methods. When this compiles down into ES6, however, we can see that TypeScript is doing exactly what our previous ES6 approach was doing:
var Thing = (function () {
function Thing(label) {
this.doAsync = bluebird.coroutine(function () {
var value;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, Promise.resolve(this.label)];
case 1:
value = _a.sent();
return [2 /*return*/, (value)];
}
});
});
this.label = label;
}
return Thing;
}());
As you can see, TypeScript took our "inline" Coroutine definition and moved it to the constructor function, not to the Prototype. Of course, since we don't have to look at the compiled code, this movement should be transparent; but, it's good to know that this transportation happens so that you don't have any illusions as to where the Coroutine is being defined and how many instances of it exist.
ASIDE: I hypothesize that the inline properties are migrated to the Constructor - and not to the Prototype - so that Fat Arrow functions can be used to create instance-bound methods that can be passed-around as naked Function references.
Of course, if you can use TypeScript, you can always use the Async / Await syntax:
// Require the core node modules.
var bluebird = require( "bluebird" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// Define the Thing class.
class Thing {
public label: string;
// I initialize the Thing class.
constructor( label: string ) {
this.label = label;
}
// In TypeScript, you can use the Async / Await syntax before it is actually
// available in your particular runtime.
async doAsync() : Promise<string> {
var value = await Promise.resolve( this.label );
return( value );
}
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
var thing = new Thing( "woot 6" );
thing
.doAsync()
.then(
function handleResolve( value: string ) {
console.log( "[TypeScript] Do Async:", value );
}
)
;
This is literally the first time that I've ever tried using Async / Await, so I can't speak much to it. I know that it behaves very much like Generators and Yield with some subtle differences. But, if we look at how this code gets compiled to ES6, we can see that it looks a lot like one of our previous ES6 examples:
var Thing = (function () {
function Thing(label) {
this.label = label;
}
Thing.prototype.doAsync = function () {
return __awaiter(this, void 0, void 0, function () {
var value;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, Promise.resolve(this.label)];
case 1:
value = _a.sent();
return [2 /*return*/, (value)];
}
});
});
};
return Thing;
}());
From what it looks like, it appears that the doAsync() method is defined on the Prototype; but, that the doAsync() method does nothing more than turn around and both define and consume some sort of yield-inspired Generator polyfill. This is akin to what we were doing in our 3rd ES6 example above: calling Bluebird.coroutine() inside each call to doAsync().
NOTE: Async / Await is natively available in Node.js 7.6.
In the end, unless you're using the native Async / Await functionality, it looks like defining a Coroutine as a Class method requires some sort of compromise. That compromise may skew more towards ease-of-use or it may skew more towards lower-overhead. And, to be honest, I don't know if performance and overhead is even worth considering in this context. That's not to say that performance is irrelevant; just that I have no idea if the cost-overhead of defining generator functions is large enough to be anything more than a blip on the overall cost of an application.
Personally, I think I would go with either of the TypeScript approaches, if I could use TypeScript; or, if I had to use native Node.js (but am not yet on a version that supportes Async / Await), I'd probably go with the 3rd approach that re-defines and invokes the Coroutine inside each call to the doAsync() calls. This may be the least efficient approach; but, it I think it is the easiest to write and maintain, which makes it the most pragmatic in my mind.
What do you think? Are there any alternative approaches that you would recommend?
Want to use code from this post? Check out the license.
Reader Comments