ParenscriptClassicOO
JavaScript's support for object-oriented programming is a bit unusual and often feels repetitive and low-level. With ParenScript, all it takes is a few simple macros and you can bury the details of JavaScript's OO machinery.

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