Packages
The usual approach to creating packages in JavaScript is to use plain-old objects as namespaces. However, to avoid accidentally clobbering a package when it is declared again, it's common to test for the package's existence first. This macro does just that:
(defjsmacro defpackage (name) `(if (= (typeof ,name) "undefined") (setf ,name (new (*Object)))))
Classes
Classes in JavaScript are just functions that are invoked with the "new" operator. Defining a class is pretty simple, but supporting inheritance makes things a bit trickier. This class macro sets us up for inheritance (using the :extends keyword argument) by saving a copy of the superclass in the prototype and supporting a special constructor argument, "noinit", to prevent the constructor from running multiple times when the class is extended. The actual constructor implementation is moved to a method called "init", which will be called only if it exists.
(defjsmacro defclass (name &key extends) `(progn (setf ,name (lambda () (if (and (!= (slot-value arguments 0) "noinit") this.init) (.apply this.init this arguments)))) ,@(if extends `((setf (slot-value ,name 'prototype) (new (,extends "noinit"))) (setf (slot-value (slot-value ,name 'prototype) 'super) (slot-value ,extends 'prototype))))))
Methods
Defining methods is just a matter of adding functions to the class's prototype object. Defining static (class) methods is done by adding functions to the class object itself:
(defjsmacro defmethod (cname mname args &rest body) `(setf (slot-value (slot-value ,cname 'prototype) ,mname) (lambda ,args ,@body))) (defjsmacro defstatic (cname mname args &rest body) `(setf (slot-value ,cname ,mname) (lambda ,args ,@body)))
Superclass Calls
Finally, we'll define a little helper macro for calling methods on the superclass. This allows us to handle inheritance without needing to be concerned with the details of how the superclass prototype needs to be stored and called:
(defjsmacro super (mname &rest args) `(.call (slot-value this.super ,mname) this ,@args))
Complete Implementation
Here is the entire set of macros needed, for convenience in copying to your own code:
(defjsmacro defpackage (name) `(if (= (typeof ,name) "undefined") (setf ,name (new (*Object))))) (defjsmacro defclass (name &key extends) `(progn (setf ,name (lambda () (if (and (!= (slot-value arguments 0) "noinit") this.init) (.apply this.init this arguments)))) ,@(if extends `((setf (slot-value ,name 'prototype) (new (,extends "noinit"))) (setf (slot-value (slot-value ,name 'prototype) 'super) (slot-value ,extends 'prototype)))))) (defjsmacro defmethod (cname mname args &rest body) `(setf (slot-value (slot-value ,cname 'prototype) ,mname) (lambda ,args ,@body))) (defjsmacro defstatic (cname mname args &rest body) `(setf (slot-value ,cname ,mname) (lambda ,args ,@body))) (defjsmacro super (mname &rest args) `(.call (slot-value this.super ,mname) this ,@args))
Usage
Here's a simple example, defining 2d and 3d point classes using inheritance and nested packages:
(defpackage lib) (defpackage lib.geom) (defclass lib.geom.*point-2d) (defmethod lib.geom.*point-2d 'init (x y) (setf this.x x) (setf this.y y)) (defmethod lib.geom.*point-2d 'to-string () (return (+ "Point2d(" this.x ", " this.y ")"))) (defclass lib.geom.*point-3d :extends lib.geom.*point-2d) (defmethod lib.geom.*point-3d 'init (x y z) (super 'init x y) (setf this.z z)) (defmethod lib.geom.*point-3d 'to-string () (return (+ "Point3d(" this.x ", " this.y ", " this.z ")")))
JavaScript Output
With these macros defined, ParenScript will generate the following JavaScript output from the above code:
if (typeof lib == 'undefined') { lib = new Object(); }; if (typeof lib.geom == 'undefined') { lib.geom = new Object(); }; lib.geom.Point2d = function () { if (arguments[0] != 'noinit' && this.init) { this.init.apply(this, arguments); }; }; lib.geom.Point2d.prototype.init = function (x, y) { this.x = x; this.y = y; }; lib.geom.Point2d.prototype.toString = function () { return 'Point2d(' + this.x + ', ' + this.y + ')'; }; lib.geom.Point3d = function () { if (arguments[0] != 'noinit' && this.init) { this.init.apply(this, arguments); }; }; lib.geom.Point3d.prototype = new lib.geom.Point2d('noinit'); lib.geom.Point3d.prototype.super = lib.geom.Point2d.prototype; lib.geom.Point3d.prototype.init = function (x, y, z) { this.super.init.call(this, x, y); this.z = z; }; lib.geom.Point3d.prototype.toString = function () { return 'Point3d(' + this.x + ', ' + this.y + ', ' + this.z + ')'; };
The use of these macros cuts the amount code we need to write for this example by about two thirds.
ParenScript