Whopper of a javascript extension
I consider the start of my programming carreer to be when I learned Genera LISP on Symbolics LISP machines. Sure I had coded in Basic, Pascal and C, and unfortunately Fortran, before this, but it had always just been a hobby. With LISP, I got serious about languages, algorithms, etc.
Genera LISP had its own object system called Flavors, much of which eventually made it into CLOS, the Common Lisp Object System. Flavors had capabilities called Wrappers and Whoppers, which provided aspect oriented capabilities before that term was even coined. Both achieved fundamentally the same goals, to wrap a function call with pre and post conditions, including preventing the underlying function call from occuring. Wrappers achieved this via LISP macros, i.e. the calls they wrapped were compiled into new calls, each call using the same wrapper sharing zero code. Whoppers did the same thing except dynamically, allowing the sharing of whopper code, but also requiring at least two additional function calls at runtime for every whopper.
So what's all this got to do with javascript? Well, yesterday I got tired of repeating myself in some CPS node coding and just turn my continuation into a new continuation wrapped with my common post condition, and so I wrote the Whopper capability for javascript. But first a detour through CPS land and how it can force you to violate DRY.
CPS means saying the same thing multiple times
So in a normal synchronous workflow you might have some code like this:
function getManifest(refresh) {
if(!refresh && _manifest) {
return _manifest;
}
var manifest = fetchManifest();
if(!_manifest) {
var pages = getPages();
_manifest = buildManifest(pages);
} else {
_manifest = manifest;
if(refresh) {
var pages = getPages();
updateManifest(pages);
}
}
saveManifest(manifest);
return _manifest;
};
But with CPS style asynchrony you end up with this instead:
function getManifest(refresh, continuation, err) {
if(!refresh && _manifest) {
continuation(_manifest);
return;
}
fetchManifest(function(manifest) {
if(!_manifest) {
getPages(function(pages) {
_manifest = buildManifest(pages);
saveManifest(_manifest,function() {
continuation(_manifest);
});
}, err);
return;
}
_manifest = manifest;
if(refresh) {
getPages(function(pages) {
updateManifest(pages);
saveManifest(_manifest,function() {
continuation(_manifest);
});
}, err);
} else {
saveManifest(_manifest,function() {
continuation(_manifest);
});
}
}, err);
};
Because the linear flow is interrupted by asynchronous calls with callbacks, our branches no longer converge, so the common exit condition, saveManifest & return the manifest, is repeated 3 times.
While I can't stop the repetition entirely, I could at least reduce it by capturing the common code into a new function. But even better, how about I wrap the original continuation with the additional code so that I can just call the continuation and it runs the save as a precondition:
function getManifest(refresh, continuation, err) {
if(!refresh && _manifest) {
continuation(_manifest);
return;
}
continuation = continuation.wrap(function(c, manifest) { saveManifest(manifest, c); });
fetchManifest(function(manifest) {
if(!_manifest) {
getPages(function(pages) {
_manifest = buildManifest(pages);
continuation(_manifest);
}, err);
return;
} else {
_manifest = manifest;
if(refresh) {
getPages(function(pages) {
updateManifest(pages);
continuation(_manifest);
}, err);
} else {
continuation(_manifest);
}
}, err);
};
Finally! The wrap function...
What makes this capture possible is this extension to the Function prototype:
Object.defineProperty(Function.prototype, "wrap", {
enumerable: false,
value: function(wrapper) {
var func = this;
return function() {
var that = this;
var args = arguments;
var argsArray = [].slice.apply(args);
var funcCurry = function() {
func.apply(that, args);
};
argsArray.unshift(funcCurry);
wrapper.apply(that, argsArray);
};
}
});
It rewrites the function as a new function that when called will call the passed wrapper function with a curried version of the original function and the arguments passed to the function call. This allows us to wrap any pre or post conditions, including pre-conditions that initiate asynchronous calls themselves, and even lets the wrapper function inspect the arguments that the original function will be passsed (assuming the wrapper decides to call it via the curried version.
The above overwrite the original continuation
with a wrapper version of itself. The wrapper is passed c
, the curried version of the original function and the argument that continuation
is called with, which we know will be the manifest. The wrapper in turn calls the async function saveManifest
with the passed manifest and passes the curried continuation
as its continuation. So when we call continuation(_manifest)
, first saveManifest
is called which then calls the original continuation with the _manifest
argument as well.