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.
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);
};
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.
continuation = continuation.wrap(function(c, manifest) {
saveManifest(manifest, c);
});
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.
So there I was trying to get jade-mode running and it kept dropping into fundamental mode. The error it gave in *Messages* was:
File mode specification error: (void-function whitespace-mode)
Problem was i was running emacs-22.2.3, the latest in centos and thereby the AWS Linux AMI. And whitespace-mode, the major mode that jade-mode and stylus-mode rely on requires emacs-23. Fine, tell me what repo to add and let me get on with my life. What, no rpm's?
That's where i usually draw the line and say to myself that it's something that i probably don't need. Over the years, i've developed an aversion to building from source, if only because then i have software on my machine that other rpm's can't count on as pre-requisites. But this time, that wasn't gonn fly. I wanted jade-mode!
As my usual build recipies go, this is what i had to do on an existing AWS Linux AMI, so some of the yum deps are missing. Don't worry, when you run ./configure it'll bitch and it'll usually be some *-devel package. So here goes building from source:
yum -y install ncurses-devel cd /tmp wget http://ftp.gnu.org/pub/gnu/emacs/emacs-23.3a.tar.gz tar zxf emacs-23.3a.tar.gz cd emacs-23.3 ./configure --prefix=/usr/local --with-xpm=no make make install