JOMP -- The Javascript One-metaclass Metaobject Protocol

This page was put together to demonstrate my metaobject protocol design for JavaScript. Here are some key links:

JavaScript is a powerful programming language that includes many of the same metaprogramming features that can be found in languages like Ruby, often with a more elegant syntax. In Mozilla's Rhino JS implementation, it adds Java's formidable set of libraries to the mix, making it a powerful contender in the recent battles among scripting languages.

JavaScript has a very simple and elegant design at its core. An interesting characteristic of JavaScript objects is that they are general collections of properties, a feature that Groovy has tried to copy with its Expando objects.

The beauty of this design, as far as designing an MOP is concerned, is that we only need a single metaclass (or whatever you want to call it for a prototype-based language). By modifying the behavior of objects, we can achieve a great deal of additional power. In fact, we can add a wealth of metaprogramming techniques to the language simply by intercepting the setting and getting of properties.

A metafunction object could be handy as well, but as we will see, it is not required. Because all functions are properties, we can modify their behavior by creating wrapper functions. Even global functions are treated as properties of the global object.

Additions to the Language

A new __metabobject__ property is introduced to the language. This dictates the rules for the behavior of the object. If this is not found, the default behavior is standard JavaScript. The metaobject can specify the following behaviors:

An alternate approach would have been to add these methods to the Object prototype itself. This is more or less the approach taken with Java 6's JSAdapter object. The benefit of the __metaobject__ property is that it provides a logical way of grouping together a collection of these methods.

Basic Setter/Getter Stuff

In this section, we'll cover some of the basic cases where we intercept properties. In Java and other languages, you could achieve much the same result by using a setter and getter. However, the key difference here is that we may decide to change the behavior at runtime, something that many languages cannot do easily. `ant getter-setter-examples` will run the examples from this section.

Read-only properties

For a simple example, let's create a new employee:

    function Employee(firstName, lastName, salary) {
      this.firstName = firstName;
      this.lastName = lastName;
      this.salary = salary;
    }

    var t = new Employee('Tom', 'Austin', 1000000);
    print(t.firstName + " " + t.lastName + " $" + t.salary);
  

After creating this employee, we may want to prevent the salary field from being changed accidentally. To do this, we can change the rules for setting the salary property:

    //Now we want to make salary read only
    var mop = {};
    mop.set = function(thisObj,prop,value) {
      if (prop == 'salary') {
        throw new Error('Warning: Salary is a read-only property');
      }
      thisObj[prop]=value;
    }
    t.__metaobject__ = mop;
  

After this, any attempt to change the salary will not work.

    //This will print an error and the salary will not be changed.
    try {
      t.salary = 999999;
    }
    catch (e) {
      print(e);
    }
    print("After an attempt to change the salary, it is: $" + t.salary);
  

Calculated properties

Over time, the definition for a field might change. For example, salary could include a bonus, but you might still want salary to refer to the total salary. With a change to the object's behavior, this is easily done:

    //Change salary to use baseSalary and bonusPay
    t.baseSalary = 1000000;
    t.bonusPay = 500;
    t.__metaobject__.get = function(thisObj,prop) {
      if (prop == 'salary') {
        return thisObj.baseSalary + thisObj.bonusPay;
      }
      else return thisObj[prop];
    }
    print("Old Salary: $" + t.salary);
    t.bonusPay += 2000;
    print("New Salary: $" + t.salary);
  

Tracing

Logging is a common use-case given for metaobject protocols. Often you would like to trace an object's behavior for troubleshooting. One common method is to insert print statements, but this clutters up the code. More importantly, it might clutter up the logs as well, making it harder for you to spot the problem.

Metaobject protocols offer a good solution to this problem. The code to an object can be left unchanged, but you can change its behavior to report back detailed messages.

An important point here is that the object's behavior can be changed on the fly, so you can limit the verbose logging to only a portion of the code. Also, you can alter the behavior of only a given object or a whole group of objects just as easily.

Here is an example function that will trace an object's behavior:

    function traceObject(o, objName) { 
      var oldMo = o.__metaobject__;
      var tracingMO = {};

      // This function can be used to disable a tracing routine.
      tracingMO.stopTrace = function() {
        o.__metaobject__ = oldMo;
      }

      // Logs the getting of properties.  Functions returned
      // will print their property 
      tracingMO.get = function(thisObj,prop) {
        logMessage("***Getting " + prop + " from " + objName);

        var returnVal = thisObj[prop];
        if (oldMo) returnVal = oldMo.get(thisObj,prop);

        //We'll wrap functions so that we know when they are called.
        if ((typeof returnVal) == "function") {
          var wrapFunct = function() {
            var msg = "***Calling " + prop + " with args:";
            for (var i=0; i<arguments.length; i++) {
              msg += " " + arguments[i];
            }
            logMessage(msg);
            returnVal.apply(thisObj, arguments);
          }
          return wrapFunct;
        }
        else return returnVal;
      }

      // Logs the setting of properties
      tracingMO.set = function(thisObj,prop,value) {
        logMessage("***Setting " + objName + "'s " + prop + " to '" + value + "'");

        if (oldMo) oldMo.set(thisObj,prop,value);
        else thisObj[prop] = value;
      }

      o.__metaobject__ = tracingMO;
    }
  

There are a few key points to note in this example. First of all, the original object might have its own __metaobject__. We don't want to lose that, so we must wrap the tracing functions around the original. Also, since the original might not have a __metaobject__ specified, we have to consider that case as well.

We want to be able to track when a function is called and with what arguments. To do this, we can wrap the original function in a new one and return that on the fly.

This highlights a couple of the downsides to not having a __metafunction__ property as well. First of all, constructing the new functions on the fly can be expensive. For troubleshooting, that is probably acceptable.

Another, more subtle problem is that the new function can be treated as an object. It is possible that it might be passed as an argument to another function, stored as a property for another object, etc. At that point, the function is no longer under the control of the tracing metaobject. Turning off the tracing behavior won't affect the new function.

These issues are relatively minor, but they are points to consider.

Now, to see an example of how this tracing function works, you can run `ant tracing`. This will trace the behavior of Rincewind:

    var rincewind = {};
    traceObject(rincewind, "Rincewind"); //Enables tracing

    rincewind.hatName = "Wizzard";
    rincewind.weapon = "sock & half-brick";
    rincewind.attack = function(enemyName) {
      print("Hit " + enemyName + " with " + rincewind.weapon);
    }

    rincewind.attack("Hell-Demon");
    rincewind.weapon = "other sock & half-brick";
    rincewind.attack("Nastier Hell-Demon");
  

Now, if you run this example, you will see a lot of detailed logging.

    ***Setting Rincewind's hatName to 'Wizzard'
    ***Setting Rincewind's weapon to 'sock & half-brick'
    ***Setting Rincewind's attack to '
    function (enemyName) {
        print("Hit " + enemyName + " with " + rincewind.weapon);
    }
    '
    ***Getting attack from Rincewind
    ***Calling attack with args: Hell-Demon
    ***Getting weapon from Rincewind
    Hit Hell-Demon with sock & half-brick
    ***Setting Rincewind's weapon to 'other sock & half-brick'
    ***Getting attack from Rincewind
    ***Calling attack with args: Nastier Hell-Demon
    ***Getting weapon from Rincewind
    Hit Nastier Hell-Demon with other sock & half-brick
  

However, after this, you might not care about the rest of the results. At this point, you can disable tracing:

    rincewind.__metaobject__.stopTrace();
    rincewind.weapon = "turnip";
    rincewind.attack("Evil Warlord");
  

The behavior is normal for this section, and much less verbose:

    Hit Evil Warlord with turnip
  

New Metaprogramming Magic

The previous examples are just the tip of the iceberg. JavaScript objects are general collections of properties. These properties can be functions. Combined with the ability to manage all of the properties of an object with a single method, this allows us to do some things that are not possible with most languages today.

Mimicking Ruby's method_missing

One of Ruby's unique features is 'method_missing'. This method is called whenever an unrecognized method is called on a Ruby object. It can be overridden for some neat affects. Many other languages are copying this feature. PHP, for instance, has __get, __set, and __call functions that collectively achieve the same result.

Rhino JS itself currently has __noSuchMethod__, but as it turns out, we can do the same thing a different way. With JOMP, we can achieve the same result, albeit in a slightly different manner. When an unrecognized method is called, a new function can be created and returned. `ant lisp-list` will run this example.

Here is an example adding Lisp's car and cdr family of functions to an array. We'll start with a simple array object and add the base methods:

    // Modifications to give the Array class the car/cdr abilities of Lisp.
    Array.prototype.car = function() {
       return this[0];
    }
    Array.prototype.cdr = function() {
       return this.slice(1);
    }
  

Now that car and cdr are available, we can include more sophisticated Lisp list functions like cadar and cddr. However, unlike in Lisp, there will be no limit to the available methods.

This is done by creating a new method whenever an unrecognized method is called and matches the pattern:

    var mop = {};
    mop.get = function(thisObj,propName) {
      if (propName.match(/^c(a|d)(a|d)+r$/)) {
         var list = thisObj;
         return function() {
            var chars = propName.match(/a|d/g).reverse();
            for (var i=0; i<chars.length; i++) {
               var op = chars[i];
               if (op === 'a') {
                  list = list.car();
               }
               else if (op === 'd') {
                  list = list.cdr();
               }
            }
            return list;
         }
      }
      else return thisObj[propName];
    }

    var list = [[0, [1, 2], 3], 4];
    list.__metaobject__ = mop;
  

Now list.caadar() would return 1. An easy addition would be to add this new method to the object, which would prevent the overhead of creating this method each time it is needed. (This could probably be done in Ruby as well, but would not be as easy as it is in JS).

Multiple Inheritance

With JOMP, we can change some of the core features of JavaScript. For a good example of this, we'll add multiple inheritance. To truly be multiple inheritance, we need to make the following changes:

These changes will require modifications to both an object's behavior and its prototype's behavior. To illustrate this, we'll create some prototypes needed for a role-playing game. `ant multi-inh` will run this example.

Multiple Prototype Chains

First, we need to create the base classes. We'll create one for heroes, which the user would control, and one for NPCs, which will be controlled by the computer. NPCs are further divided into allies and villains. An ally will help the hero and a villain will attack the hero.

The Hero and NPC definitions are not particularly interesting. The Ally and Villain definitions are more to the heart of the matter. These will both define a 'move' method, but will have different implementations.

    function Ally(name, hitpoints, experience, xpValue) {
      NPC.call(this, name, hitpoints, experience, xpValue);
    }
    Ally.prototype = new NPC();
    Ally.prototype.move = function() {
      print(" (" + this.name + "'s action:  Help hero)");
    }

    function Villain(name, hitpoints, experience, xpValue) {
      NPC.call(this, name, hitpoints, experience, xpValue);
    }
    Villain.prototype = new NPC();
    Villain.prototype.move = function() {
      print(" (" + this.name + "'s action:  Attack hero)");
    }
  

However, our game could use more classes than this. For instance, some characters might be able to use magic. We could have a Wizard definition like the following:

    function Wizard() {};
    Wizard.prototype.castSpell = function(spellName) {
      if (this.spells[spellName]) {
        var spell = this.spells[spellName];
        return spell();
      }
    }
  

Unfortunately, we could have wizards that are heroes, villains, or allies. In Java, the solution would be to create a Wizard interface, and then to have HeroWizard, VillainWizard, and AllyWizard implementations. Fantasy worlds being what they are, this could quickly get out of hand. We could very well end up with implementations like UndeadRedDragonWizardKingVillain. This would quickly become untenable, and a new approach would need to be designed.

This tends to be less of an issue in most scripting languages. In JavaScript, for instance, we could add mix-ins that would add all of the extra methods we needed to an object. But there are two problems with this.

The first problem is that 'instanceof' will not work as a means to identify an object's type. We could work around this by adding a method to the prototype or to the objects themselves, though this is not ideal.

A second problem is that the extra functions, once mixed-in to the object, become difficult to un-mix. This could be a problem in some cases, as we will see.

Instead, we will change the behavior of these prototypes and their instances to use an array for a prototype. All prototypes in the array will be treated as the object's prototype.

The object's behavior must be changed to use the array for both getting the ids and looking up properties:

    var objMop = {};
    objMop.getIds = function(thisObj) {
      var ids = []
      for (var ind in thisObj) {
        ids.push(ind);
      }
      if (thisObj.__proto__ instanceof Array) {
        for (var ind in thisObj.__proto__) {
          var proto = thisObj[ind];
          if (proto) {
            for (var name in proto.prototype) {
              if (!ids[name]) ids.push(name);
            }
          }
        }
      }
      return ids;
    }
    objMop.get = function(thisObj,prop) {
      if (thisObj[prop]) return thisObj[prop];
      else if (thisObj.__proto__ instanceof Array) {
        for (var ind in thisObj.__proto__) {
          var proto = thisObj.__proto__[ind];
          if (proto.prototype[prop]) {
            return proto.prototype[prop];
          }
        }
      }
      return thisObj[prop];
    }
  

We also need to change the behavior of the prototype definitions in order for 'instanceof' to work as we would like:

    var multiMop = {};
    multiMop.hasInstanceOf = function(thisObj,instance) {
      if (instance.__proto__ instanceof Array) {
        for (var key in instance.__proto__) {
          var prot = instance.__proto__[key];
          if (prot == thisObj) return true;
        }
        return false;
      }
      //Note that instanceof can be used normally inside the method.
      else return (instance instanceof thisObj);
    }
    Wizard.__metaobject__ = multiMop;
    Hero.__metaobject__ = multiMop;
    Ally.__metaobject__ = multiMop;
    Villain.__metaobject__ = multiMop;
  

Now that we have our prototype definitions and our new object behavior, we can demonstrate an example. We'll define our hero as Jason, leader of the Argonauts:

    var jason = new Hero("Jason", 10);
  

In his quest, Jason meets and later marries Medea. She is an enchantress who helps him out of several situations. (This uses Mozilla's __proto__ property to reassign the prototype chain.)

    var medea = new Ally("Medea", 4);
    medea.__metaobject__ = objMop;
    medea.__proto__ = [Ally, Wizard];
    medea.spells = {
      old2new: function(ram) { print("'Look, the ram is young now!  Let's chop up father...'"); },
      makePay: function(ex) { print("'Dear gods, no...'"); }
    };
  

Medea is an Ally. She is also a Wizard. Both `medea instanceof Ally` and `medea instanceof Wizard` will be true. When 'move' is called she will help Jason.

    medea.move(); //This will call the Ally version of the method.
    medea.castSpell('old2new'); //Medea tricks the daughters of Pelias.
  

However, it is a big world, and Jason decides to move on.

    jason.divorce = function(wife) {
      for (var i in wife.__proto__) {
        if (wife.__proto__[i] == Ally) wife.__proto__[i] = Villain; 
      }
    }
    jason.divorce(medea);  //As a general rule, don't divorce a girl who has a history of cutting up your enemies.
  

After this, 'medea' still refers to the same object. Her wizard abilities are unchanged, but she is now instanceof Villain instead of instanceof Ally.

    // Medea's move function definition now comes from Villain.
    medea.move();
    // Her other properties, like magic, remain the same.
    medea.castSpell('makePay');
  

This is a key point, and one advantage of a prototype-based object design in general. Class-based designs are great for defining static behavior, but modifying that behavior on the fly becomes more challenging. The typical solution for this example would be to create a new instance of Medea. However, any other modifications to Medea's state could be lost without careful programming. If Medea happened to be holding the goldenFleece object in her inventory, it might suddenly disappear when allyMedea was replaced by villainMedea.

Prototype-based systems don't need to worry about this. The only change to Medea is her switch from Ally to Villain. Nothing else is affected.

This type of change occurs all the time in fantasy worlds, and this solution makes that easy to model. Prince Arthas from Warcraft III could change his prototype from [Human,Paladin] to [Undead,DeathKnight] in one line of code.

Being able to compartmentalize and alter behavior at will is not needed for all problems. However, when it is, prototype chains are an ideal solution. By using a MOP to create multiple inheritance, we can make this even more powerful.

Conclusion

The neat thing about JavaScript is its Scheme-like simplicity. Because there are very few constructs to the language, we can add a fairly sophisticated MOP by the use of a single metaclass. Although operators and primitives cannot be changed by this design, almost everything else can be.