diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6072182 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +\.#* +node_modules +*~ diff --git a/README.md b/README.md index 96ebece..fbed08d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,187 @@ JS-CLOS ======= -A CLOS-like framework sketch on JavaScript. +A CLOS-like object system in JavaScript. + ++ Multiple inheritance ++ Multimethod ++ Type checking on construction + + +Usage +----- + +### Simple Data Class ### + +```javascript +//class, when `make`d, retruns a hash of values +var _book_ = define_class([], function (x) { + return slot_exists(x, 'title', "string") + && slot_exists(x, 'author', "string"); +}); + +//generic function show +var show = define_generic(); + +//show an instance of book +define_method(show, [_book_], function (b) { + return b.title + " by " + b.author; +}); + +var p_city = make(_book_, {title:'Permutation City', author:'Greg Egan'}); + +show(p_city); +``` + + +### Multimethod ### + +```javascript +//define a bunch of classes +//the name is optional +var floor = define_class([], undefined, "floor"); +var carpet = define_class([], undefined, "carpet"); +var ball = define_class([], undefined, "ball"); +var glass = define_class([], undefined, "glass"); +var stick = define_class([], undefined, "stick"); + +//function to display the result +var bumpOutput = function(x, y, result){ + console.log(x + ' + ' + y + ' bump = ' + result); +}; + +//define a generic function `bump` +var bump = define_generic(); + +//define methods +define_method(bump, [ball, floor], function(x, y){ + bumpOutput(x, y, 'bounce'); +}); +define_method(bump, [glass, floor], function(x, y){ + bumpOutput(x, y, 'crash'); +}); +define_method(bump, [stick, floor], function(x, y){ + bumpOutput(x, y, 'knock'); +}); + +//if you prefer, the following works, too +bump.defMethod([undefined, carpet], function(x, y){ + bumpOutput(x, y, 'silence'); +}); + +//call the methods +bump(new ball, new floor); //should bounce +bump(new glass, new floor); //should crash +bump(new stick, new carpet); //shold silince + +bump(new floor, new stick); // undefined method +``` + +API +--- + +### define_class ### + +Takes: ++ an arrey of classes to inherit from. *(required)* ++ a validator function called upon instance construction. *(optional)* ++ a name string which is used as the string representation of its instances *(optional)* + +Returns a constructor function (called **class**) that can be `new`ed or get applied to `make` + +#### Syntax #### +**define_class**([ *parent classes* ], function (x) { *protocol* }, *name*); + +#### Example ### + +```javascript +var x = define_class([]); +var y = define_class([x]); +var z = define_class([x, y], funciton (a) { + return slot_exists(a, 'name', 'string'); +}); +``` + +### define_generic ### + +Takes nothing. + +Returns **a generic function**. + +#### Syntax #### +**define_generic**(); + +#### Example #### + +```javascript +var show = define_generic(); +``` + +### define_method ### + +Takes: ++ **a generic function** *(required)* ++ an array of **classes** that specifies the type of arguments given to the method *(required)* ++ a function that is the body of the method *(required)* + +Returns void. + +#### Syntax #### +**define_method**( *generic function* , [ *classA* , *...* ], function ( *a* , *...* ) { *body* }); + +#### Example #### + +```javascript +define_method(show, [z], function (a) { + console.log(a.name); +}); + +define_method(show, [x], function (a) { + console.log("an instance of x"); +}); +``` + +### make ### + +Takes: ++ **a class** *(required)* ++ a hash object specifying the initial values of each slot *(optional)* + +Returns an instance of the class. + +#### Syntax #### + +**make**( *class*, { *slot_name*: *initial_value*, *...*}); + +#### Example #### + +```javascript +make(x); +make(z, {name: "foo"}); +make(z); //ERROR +``` + +#### Note #### + +The hash object given as the second argument is matched with the function given as the second argument to `define_class`. If the result is false, an exception gets thrown. + +### is_a ### + +Takes: ++ an instance *(required)* ++ a class or a string *(required)* + +Returns boolean + +### slot_exists ### + +Takes: ++ an instance *(required)* ++ a slot identifier *(required)* ++ a class or a string to specify the type *(optional)* + +Returns a boolean. True if the instance has the slot of the specified type. False otherwise. + +### defClass, defGeneric, defMethod, isA ### + +Aliaces. diff --git a/clos.js b/clos.js index ad3e24d..4ede347 100644 --- a/clos.js +++ b/clos.js @@ -4,70 +4,167 @@ * LLGPL -> http://opensource.franz.com/preamble.html */ -var CLOS = {}; -CLOS.generics = {}; - -CLOS.generic = function(name){ - this.name = name; - this.methods = []; -}; -CLOS.method = function(clause, body){ - this.clause = clause; - this.body = body; -}; -CLOS.isA = function(example, standard){ - if(standard === undefined){ - return true; - } - if(example === standard){ - return true; - } - if(typeof(example) == standard){ - return true; - } - return false; -} -CLOS.method.prototype.check = function(parameters){ - var i; - for(i in this.clause){ - if(CLOS.isA(parameters[i], this.clause[i])){ - continue; - } - return false; - } - return true; -}; - -CLOS.defGeneric = function(name){ - CLOS.generics[name] = new CLOS.generic(name); -}; -CLOS.getGeneric = function(name){ - if(!CLOS.generics[name]){ - throw 'CLOS error: generic ' + name + ' is not defined'; - } - return CLOS.generics[name]; -}; - -CLOS.defMethod = function(name, parameters, body){ - var generic = CLOS.getGeneric(name); - generic.methods[generic.methods.length] = new CLOS.method(parameters, body); -}; - -CLOS.call = function(name){ - var generic = CLOS.getGeneric(name), - parameters = Array.prototype.slice.call(arguments, 1), - method, i; - for(i in generic.methods){ - method = generic.methods[i]; - if(method.check(parameters)){ - return method.body.apply(parameters); - } - } - throw 'CLOS error: cannot find method ' + name + ' for ' + parameters; -}; - -/*CLOS.init = function(object){ - object.clos = {}; - object.clos.prototype = CLOS; - object.clos.object = object; -};*/ +if ( ! Array.prototype.forEach) + Array.prototype.forEach = function (f) { + var i = 0, l = this.length; + for (; i < l; ++i) + f(this[i], i); + }; + + +module.exports = (function () { + var CLOS = {}; //exported namespace + + var _slice = Array.prototype.slice; + + CLOS.options = {}; + CLOS.options.dispatchBasedOnSpecificity = true; + + //JS class + + /* constructor for generic-function object */ + function Generic () { + + var self = function () { + return _call.call(self, _slice.call(arguments)); + }; + + self.defMethod = function (parameters, body) { + self.methods.push(new Method(parameters, body)); + if (CLOS.options.dispatchBasedOnSpecificity) + self.methods.sort(specificity); + }; + + self.methods = []; + return self; //this is valid + }; + + //sort function + function specificity (a, b) { + var aWin = 0, bWin = 0, i = 0, l = a.clause.length; + for (; i < l; ++i) { + if (CLOS.isA(a.clause[i], b.clause[i])) ++bWin; + if (CLOS.isA(b.clause[i], a.clause[i])) ++aWin; + } + return aWin - bWin; + }; + + /* constructor for actual method generic functions delegates to */ + function Method (clause, body){ + this.clause = clause; + this.body = body; + }; + + Method.prototype.check = function(parameters){ + var i, self = this; + for(i in this.clause){ + if (CLOS.isA(parameters[i], this.clause[i])) + continue; + return false; + } + return true; + }; + + /* -- /Method -- */ + + /* classes are constructor functions */ + /* The constructor may take a predicate function that ensures its instances + * to have specific properties */ + CLOS.defClass = function (parents, pred, name) { + pred = pred || function () {return true;}; + var cl = function (obj) { + var key; + parents.forEach(function (p) { return p._pred(obj); }); //check for exception + if ( ! pred(obj)) throw "Initialization error"; + for (key in obj) + if (obj.hasOwnProperty(key)) + this[key] = obj[key]; + }; + var flatParents = parents.reduce(function (acc, cur) { + return acc.concat(cur._parents); }, parents); + cl._pred = pred; + cl.prototype.constructor = cl; + cl._parents = flatParents; + cl.prototype._parents = flatParents; + cl.prototype.toString = function () { return name || JSON.stringify(this); }; + cl.prototype.isA = function (standard) { return CLOS.isA(this, standard); }; + return cl; + }; + + //procedures + + //more like a pattern-matching + /** + * passes when: + * example === standard + * standard === undefined + * typeof(example) == standard + * example instanceof standard + * member(example._parent, standard) + */ + CLOS.isA = function (example, standard) { + if (example === standard) return true; + if (! example) return false; + switch(typeof(standard)) { + case "undefined": + return true; + case "string": + return (typeof(example) == standard); + case "function": + case "object": + return (example instanceof standard) + || hasParent(example._parents, standard); + default: + return false; + } + }; + + function hasParent (parents, standard) { + if ( ! parents) return false; + return parents.indexOf(standard) > -1; + }; + + + /* (define-generic) */ + CLOS.defGeneric = function () { + return new Generic(); + }; + + //alias + //this function is expensive + CLOS.defMethod = function (generic, params, body) { + generic.defMethod(params, body); + }; + + var _call = function (parameters) { + var method, i; + //iterate over methods defined on the generic + for(i in this.methods){ + method = this.methods[i]; + //checks if the given parameter matches the declared type + if(method.check(parameters)){ + return method.body.apply({}, parameters); + } + } + throw 'CLOS error: cannot find specified method for ' + parameters; + }; + + //for schemer + CLOS.define_method = CLOS.defMethod; + CLOS.define_generic = CLOS.defGeneric; + CLOS.define_class = CLOS.defClass; + CLOS.is_a = CLOS.isA; + + CLOS.slot_exists = function (obj, slot, cls) { + return (obj[slot] !== undefined) + && cls ? CLOS.isA(obj[slot], cls) : true; + }; + + //alias to `new` + CLOS.make = function (cls, obj) { + return new cls(obj); + }; + + return CLOS; + +}()); diff --git a/test.js b/test.js index 41a0b79..fd6a752 100644 --- a/test.js +++ b/test.js @@ -1,56 +1,135 @@ +var CLOS = require('./clos'); + // our domain -var floor = {}; -var carpet = {}; -var ball = {}; -var glass = {}; -var stick = {}; +var floor = CLOS.defClass([], undefined, "floor"); +var carpet = CLOS.defClass([], undefined, "carpet"); +var ball = CLOS.defClass([], undefined, "ball"); +var glass = CLOS.defClass([], undefined, "glass"); +var stick = CLOS.defClass([], undefined, "stick"); var bumpOutput = function(x, y, result){ - alert(x + ' + ' + y + ' bump = ' + result); + console.log(x + ' + ' + y + ' bump = ' + result); }; var errorOutput = function(error){ - alert('[error] ' + error); + console.log('[error] ' + error); }; // definitions -CLOS.defGeneric('bump'); -CLOS.gefMethod('bump', [ball, floor], function(x, y){ - bumpOutput(x, y, 'bounce'); +var bump = CLOS.defGeneric(); + +CLOS.defMethod(bump, [ball, floor], function(x, y){ + bumpOutput(x, y, 'bounce'); +}); +CLOS.defMethod(bump, [glass, floor], function(x, y){ + bumpOutput(x, y, 'crash'); +}); +CLOS.defMethod(bump, [stick, floor], function(x, y){ + bumpOutput(x, y, 'knock'); +}); +CLOS.defMethod(bump, [undefined, carpet], function(x, y){ + bumpOutput(x, y, 'silence'); +}); + +/* //equiv to +CLOS.defMethod(bump, [undefined, undefined], function (x, y) { + bumpOutput(x, y, ''); +}); +*/ + +var Book = CLOS.defClass([], function (x) { + return CLOS.slot_exists(x, 'title', 'string') + && CLOS.slot_exists(x, 'author', 'string'); }); -CLOS.gefMethod('bump', [glass, floor], function(x, y){ - bumpOutput(x, y, 'crash'); +var Flammable = CLOS.defClass([], function (x) { + return CLOS.slot_exists(x, 'burnTime', 'number'); }); -CLOS.gefMethod('bump', [stick, floor], function(x, y){ - bumpOutput(x, y, 'knock'); +var Magazine = CLOS.defClass([Book, Flammable]); + +var show = CLOS.defGeneric(); + +CLOS.defMethod(show, [Book], function (b) { + console.log(b.title + " by " + b.author); +}); + +var burn = CLOS.defGeneric(); + +CLOS.defMethod(burn, [Magazine], function (m) { + console.log(m.title + " burnt in " + m.burnTime + " seconds."); }); -CLOS.gefMethod('bump', [undefined, carpet], function(x, y){ - bumpOutput(x, y, 'silence'); +CLOS.defMethod(burn, [Flammable], function (f) { + console.log(f + " burnt in " + f.burnTime + " seconds."); }); + +//method precedence test +var Foo = CLOS.defClass([]); +var Bar = CLOS.defClass([Foo]); +var Baz = CLOS.defClass([Bar]); +var alice = CLOS.defGeneric(); +CLOS.defMethod(alice, [Bar, Foo], function () { console.log("bar foo"); }); +CLOS.defMethod(alice, [Bar, Bar], function () { console.log("bar bar"); }); +CLOS.defMethod(alice, [Baz, Baz], function () { console.log("baz baz"); }); +CLOS.defMethod(alice, [Bar, Baz], function () { console.log("bar baz"); }); + +//immidiate values +var fib = CLOS.define_generic(); +CLOS.define_method(fib, [0], function (_) { return 1; }); +CLOS.define_method(fib, [1], function (_) { return 1; }); +CLOS.define_method(fib, ["number"], function (n) { + return fib(n - 1) + fib(n - 2); }); + // test var tests = [ - function(){ - CLOS.call('bump', glass, floor); // crash - }, - function(){ - CLOS.call('bump', stick, carpet); // silence - }, - function(){ - CLOS.call('bump', floor, stick); // undefined method - }, - function(){ - CLOS.call('put', glass, floor); // undefined generic - } + function () { + bump(new ball, new floor); //bounce + }, + function(){ + bump(new glass, new floor); // crash + }, + function(){ + bump(new stick, new carpet); // silence + }, + function(){ + bump(new floor, new stick); // undefined method + }, + function () { + bump(new ball, new floor); //bounce + }, + + function () { + show(CLOS.make(Book, {title:'Permutation City', author:'Greg Egan'})); + //Permutation City by Greg Egan + }, + function () { + CLOS.make(Book, {}); //Initialization error + }, + function () { + show(CLOS.make(Magazine, {title:'Foo', author:'Bar', burnTime:5000})); + //Foo by Bar + }, + function () { + burn(CLOS.make(Flammable, {burnTime: 5, name: "gas tank"})); + burn(CLOS.make(Magazine, {burnTime: 20, title:"Foo", author:"Bar"})); + }, + function () { + alice(CLOS.make(Baz), CLOS.make(Baz)); //baz baz + alice(CLOS.make(Baz), CLOS.make(Bar)); //bar bar + alice(CLOS.make(Baz), CLOS.make(Foo)); //bar foo + alice(CLOS.make(Bar), CLOS.make(Baz)); //bar baz + }, + function () { + console.log(fib(10)); + } ]; for(var i in tests){ - var test = tests[i]; - try{ - test(); - } - catch(error){ - errorOutput(error); - } + var test = tests[i]; + try{ + test(); + } + catch(error){ + errorOutput(error); + } }