A couple years ago, I read an article in Dr. Dobb's Journal on a next-generation programming language called D. It was probably the only article in DDJ that I fully understood...
One of the concepts in the D language is “design-by-contract”. It was the original inspiration for my assert() function, which I rewrote to take advantage of Error.prototype.stack (more on this in a minute). Any function you write may have a set of “preconditions” which must be satisfied before the main function executes, and/or “postconditions” which must be satisfied after the main function executes.
The goal of DBC is to add sanity checks for a particular function. Quality Assurance personnel and developers can add assertions that demand the code act correctly (or at least, consistently). If a precondition, postcondition, or standard assert() fails, there is a bug: either in the actual function, or in the failed contract. (But of course, the code doesn't know which!)
With a debug flag enabled, the code executing the function runs the precondition and postcondition functions. Without the debug flag, it skips them.
The example from DDJ looked something like:
function getSquareRoot(x, rv) {
in {
assert(x >= 0);
}
out {
assert(rv * rv == x);
}
body {
rv = Math->sqrt(x);
return 0;
}
}
Tackling the first was relatively easy; I'd done something like it with a earlier assert() function (also blogged here). The catch was that I never got the scope to operate in the main function. It had to pass the expression to check as a string, it had to pass a contextual “this” object, etc. I cut that out, and simply made the first argument a Boolean expression, like the condition in an if() {} statement.
Of course, there is also the added challenge of making sure the JavaScript Console gets the right filename and line number information. Constructed errors always have the Error() constructor's filename and line number. (There's a bug to have these properties set with the original throw statement, but in my case they match.) So if I was going to throw any constructed Error(), the properties of that Error() would need resetting.
Enter the stack. Before the Mozilla 1.0 release, I came up with the idea that JavaScript errors should have stacks for debugging. I floated the idea past Boris Zbarsky and a couple others, and with their general support I filed bug 123177 for it. Brendan Eich leaped on it (I very rarely see so much enthusiasm to add a feature), and implemented Error.prototype.stack to give a basic breakdown of the functions by arguments, files and lines which were running when the error was thrown.
Assume I have the following:
function foo() {
assert(1 == 0);
}function assert(mustBeTrue) {
if (mustBeTrue) return;
try {
throw new Error(“bar error”);
}
catch(e) {
dump(e.stack + “\n”);
}
}
foo();
Error(“bar error”)@:0
assert(false)@chrome://foo/content/foo.xml:13
foo()@chrome://foo/content/foo.xml:7
@chrome://foo/content/foo.xml:19
So, to reset the Error() object's properties appropriately, I wrote this little function:
Error.prototype.shiftStack = function(aShiftLines) {
var aStackArray = this.stack.split("\n").splice(aShiftLines);
var lastColon = aStackArray[0].lastIndexOf(":");
this.fileName = aStackArray[0].substring(aStackArray[0].indexOf("@") + 1, lastColon);
this.lineNumber = aStackArray[0].substr(lastColon + 1);
this.stack = aStackArray.join("\n");
}
Thus, when I write:
assert(1 == 0, “1 does not equal 0.”, true)
AssertionError: 1 does not equal 0.
Filename: chrome://foo/content/foo.xml Line: 7
A good DBC code, in JavaScript, might look like this:
function getSquareRoot(x) {
return Math.sqrt(x);
}
getSquareRoot.precondition = function(x) {
assert(x >= 0, “x must be a real number greater than zero!”, true);
}
getSquareRoot.postcondition = function(x, rv) {
assert(rv * rv == x, “The returned value times itself does not equal the original argument!”, true);
}
I created an applyContract() function:
function applyContract(aFunction, thisObj) {
var args = [];
for (var x = 2; x < arguments.length; x++) {
args[x - 2] = arguments[x]; // we're just setting up arguments to pass.
}
if (typeof aFunction.precondition == 'function') {
aFunction.precondition.apply(thisObj, args);
}
var rv = aFunction.apply(thisObj, args);
if (typeof aFunction.postcondition == 'function') {
args.push(rv);
aFunction.postcondition.apply(thisObj, args);
}
return rv;
}
var root_3 = applyContract(getSquareRoot, this, 3); // == Math.sqrt(3) in the end...
What if you want to construct an object, and run that through preconditions and postconditions?
First, you need a function that can construct the object when called by the apply() method of functions. (Figuring out how to do so is obvious only in hindsight.)
function constructNew(aFunction) {
var args = [];
var argString = "";
for (var x = 1; x < arguments.length; x++) {
args[x - 1] = arguments[x];
argString += ",args[" + (x - 1) + "]";
}
argString = argString.substr(1);
alert("var rv = new aFunction(" + argString + ")");
eval("var rv = new aFunction(" + argString + ")");
return rv;
}
var x = constructNew(Array, 2, 4, 6);
is the same as saying
x = new Array(2, 4, 6);
The major difference is I can call constructNew.apply(thisObj, [Array, 2, 4, 6]). The thisObj variable gets ignored!
So, to apply the contract while constructing an object:
function applyContractConstructor(aFunction) {
var args = [];
for (var x = 1; x < arguments.length; x++) {
args[x - 1] = arguments[x];
}
if (typeof aFunction.precondition == 'function') {
aFunction.precondition.apply(this, args);
}
args.unshift(aFunction);
var rv = constructNew.apply(thisObj, args);
args.shift(aFunction);
if (typeof aFunction.postcondition == 'function') {
args.push(rv);
aFunction.postcondition.apply(this, args);
}
return rv;
}
I'll say this, though: DBC and assert() are coming in very handy in developing Abacus! As I said earlier, if a contract fails, there's a bug in either the executing code or the contract, and it behooves me to fix it!
Posted by WeirdAl at May 17, 2004 4:20 PMconstructNew is a bit wordy, what about create or build or something else that matches up with new?
Posted by: Eric Hodel at May 18, 2004 8:23 AMThe name itself matters a little, but not too much... if you want build, that sounds all right to me.
Posted by: Alex Vincent at May 18, 2004 3:44 PM