EXTENDS challenge: preprocessor function macros and class-like oop

Background

I've been using the C preprocessor to manage and "compile" semi-large javascript projects with multiple files and build targets. This gives full access to C preprocessor directives like #include, #define, #ifdef, etc. from within javascript. Here's a sample build script so you can test the example code:

#!/bin/bash
export OPTS="-DDEBUG_MODE=1 -Isrc"
for FILE in `find src/ | egrep '\.js?$'`
do
  echo "Processing $FILE"
  cat $FILE  \
  | sed 's/^\s*\/\/#/#/'  \
  | cpp $OPTS  \
  | sed 's/^[#:<].*// ; /^$/d'  \
  > build/`basename $FILE`;
done

Make a src and a build directory, and put the .js files in src.


Convenience Macros

Originally, I just wanted the preprocessor stuff for #include and maybe a few #ifdefs, but I got to thinking, wouldn't it be nice to have some convenience macros too? Experimentation ensued.

#define EACH(o,k)     for (var k in o) if (o.hasOwnProperty(k))

Cool, so now I can write something like this:

EACH (location, prop) {
  console.log(prop + " : " location[prop]);
}

And it will expand to:

for (var prop in location) if (location.hasOwnProperty(prop)) {
  console.log(prop + " : " location[prop]);
}

How about foreach?

#define FOREACH(o,k,v)   var k,v; for(k in o) if (v=o[k], o.hasOwnProperty(k))
// ...
FOREACH (location, prop, val) { console.log(prop + " : " + val) }

Notice how we sneak v=o[k] inside the if condition so it doesn't disturb the curly braces that should follow the invocation of this macro.


Class-like OOP

Let's start with a NAMESPACE macro and an obscure but useful js pattern...

#define NAMESPACE(ns)    var ns = this.ns = new function()

new function(){ ... } does some neat stuff. It calls an anonymous function as a constructor, so it doesn't need an extra () at the end to call it, and within it this refers to the object being created by the constructor, in other words, the namespace itself. This also allows us to nest namespaces within namespaces.

Here is my full set of class-like OOP macros:

#define NAMESPACE(ns) var ns=this.ns=new function()

#define CLASS(c)      var c=this;new function()

#define CTOR(c)       (c=c.c=this.constructor=$$ctor).prototype=this;\
                      function $$ctor

#define PUBLIC(fn)    this.fn=fn;function fn
#define PRIVATE(fn)   function fn
#define STATIC(fn)    $$ctor.fn=fn;function fn

As you can see, these macros define many things both in the Variable Object (for convenience) and in this (from necessity). Here's some example code:

NAMESPACE (Store) {

  CLASS (Cashier) {

    var nextId = 1000;

    this.fullName = "floater";

    CTOR (Cashier) (fullName) {
      if (fullName) this.fullName = fullName;
      this.id = ++nextId;
      this.transactions = 0;
    }

    PUBLIC (sell) (item, customer) {
      this.transactions += 1;
      customer.inventory.push(item);
    }

    STATIC (hire) (count) {
      var newCashiers = [];
      for (var i=count; i--;) {
        newCashiers.push(new Cashier());
      }
      return newCashiers;
    }
  }

  CLASS (Customer) {

    CTOR (Customer) (name) {
      this.name = name;
      this.inventory = [];
      this.transactions = 0;
    }

    PUBLIC (buy) (item, cashier) {
      cashier.sell(this, item);
    }
  }
}

What about EXTENDS?

So this brings me to the question... how can we implement EXTENDS as a macro to wrap the usual "clone the prototype, copy constructor properties" js prototype inheritance? I haven't found a way to do it outside of requiring the EXTENDS to appear after the class definition, which is silly. This experiment needs EXTENDS or it's useless. Feel free to change the other macros as long as they give the same results.

Edit - These might come in handy for EXTENDS; listing them here for completeness.

#define EACH(o,k)   for(var k in o)if(o.hasOwnProperty(k))
#define MERGE(d,s)  EACH(s,$$i)d[$$i]=s[$$i]
#define CLONE(o)    (function(){$$C.prototype=o;return new $$C;function $$C(){}}())

Thanks in advance for any help, advice, or lively discussion. :)

Answers:

Answer

I think I just completed my own challenge. I've added a second (optional) argument to the CLASS declaration macro for the superclass of the class being declared.

My original implementation created a lot of inline junk around the constructor, so I decided to wrap some convenience functions up in a macro helper object to avoid redundancy.

Here are the current incarnations of my class-like OOP macros:

// class-like oo

#ifndef BASE
  #define BASE  $$_
#endif

#define COLLAPSE(code)      code

#define NAMESPACE(ns)       var ns=BASE._ns(this).ns=new function()

#define CLASS(c,__ARGS...)  var c=[BASE._class(this),[__ARGS][0]]; \
                            new function()

#define CTOR(c)             BASE._extend($$_##c,c[1],this); \
                            c=c[0].c=$$_##c; function $$_##c

#define PUBLIC(fn)          BASE._public(this).fn=fn;function fn

#define PRIVATE(fn)         function fn

#define STATIC(fn)          BASE._static(this).fn=fn;function fn

// macro helper object

COLLAPSE(var BASE=new function(){

  function Clone(){};

  function clone (obj) {
    Clone.prototype=obj; return new Clone;
  };

  function merge (sub, sup) { 
    for (var p in sup) if (sup.hasOwnProperty(p)) sub[p]=sup[p]; 
  };

  this._extend = function (sub, sup, decl) {
    if (sup) {
      merge(sub, sup);
      sub.prototype=clone(sup.prototype);
      sub.prototype.constructor=sub;
    };
    if (decl) {
      merge(sub.prototype, decl);
      decl._static=sub;
      decl._public=sub.prototype;
    };
  };

  this._static=this._ns=this._class=function (obj) {
    return (obj._static || obj); 
  };

  this._public=function (obj) {
    return (obj._public || obj); 
  };

})

... here's a test namespace ...

//#include "macros.js"

NAMESPACE (Store) {

  CLASS (Cashier) {

    var nextId = 1000;

    this.fullName = "floater";

    CTOR (Cashier) (fullName) {
      if (fullName) this.fullName = fullName;
      this.id = ++nextId;
      this.transactions = 0;
    }

    PUBLIC (sell) (item, customer) {
      this.transactions += 1;
      customer.inventory.push(item);
    }

    STATIC (hire) (count) {
      var newCashiers = [];
      for (var i=count; i--;) {
        newCashiers.push(new Cashier());
      }
      return newCashiers;
    }
  }

  // Customer extends Cashier, just so we can test inheritance

  CLASS (Customer, Cashier) {

    CTOR (Customer) (name) {
      this.name = name;
      this.inventory = [];
      this.transactions = 0;
    }

    PUBLIC (buy) (item, cashier) {
      cashier.sell(this, item);
    }

    CLASS (Cart) {

      CTOR (Cart) (customer) {
        this.customer = customer;
        this.items = [];
      }
    }

  }
}

... and here's the output ...

var $$_=new function(){ function Clone(){}; function clone (obj) { Clone.prototype=obj; return new Clone; }; function merge (sub, sup) { for (var p in sup) if (sup.hasOwnProperty(p)) sub[p]=sup[p]; }; this._extend = function (sub, sup, decl) { if (sup) { merge(sub, sup); sub.prototype=clone(sup.prototype); sub.prototype.constructor=sub; }; if (decl) { merge(sub.prototype, decl); decl._static=sub; decl._public=sub.prototype; }; }; this._static=this._ns=this._class=function (obj) { return (obj._static || obj); }; this._public=function (obj) { return (obj._public || obj); }; }
var Store=$$_._ns(this).Store=new function() {
  var Cashier=[$$_._class(this),[][0]]; new function() {
    var nextId = 1000;
    this.fullName = "floater";
    $$_._extend($$_Cashier,Cashier[1],this); Cashier=Cashier[0].Cashier=$$_Cashier; function $$_Cashier (fullName) {
      if (fullName) this.fullName = fullName;
      this.id = ++nextId;
      this.transactions = 0;
    }
    $$_._public(this).sell=sell;function sell (item, customer) {
      this.transactions += 1;
      customer.inventory.push(item);
    }
    $$_._static(this).hire=hire;function hire (count) {
      var newCashiers = [];
      for (var i=count; i--;) {
        newCashiers.push(new Cashier());
      }
      return newCashiers;
    }
  }
  var Customer=[$$_._class(this),[Cashier][0]]; new function() {
    $$_._extend($$_Customer,Customer[1],this); Customer=Customer[0].Customer=$$_Customer; function $$_Customer (name) {
      this.name = name;
      this.inventory = [];
      this.transactions = 0;
    }
    $$_._public(this).buy=buy;function buy (item, cashier) {
      cashier.sell(this, item);
    }
    var Cart=[$$_._class(this),[][0]]; new function() {
      $$_._extend($$_Cart,Cart[1],this); Cart=Cart[0].Cart=$$_Cart; function $$_Cart (customer) {
        this.customer = customer;
        this.items = [];
      }
    }
  }
}

Inheritance, internal classes, and nested namespaces seem to work fine. What do you think, is this a useful approach to class-like OOP and code reuse in js? Let me know if I've missed anything.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.