delegate(), delegate(), delegate()

My MBP batteries keep dying after about a year (each). I usually have 2 that I tote around with me, and each tends to be good for 1.5-2hrs of actual work. This means that I tend not to be able to work through a cross-country flight, and particularly not if I need a VM for anything (which is most of the time). I think that if Apple does rev the MBP’s on the 14th, the things I’d pay for boil down to “more memory and much longer battery life”. The 5+ hour flight to TAE then provided a short window to do work in before I retreated to watching episodes of The Colbert Report on my phone. Knowing that i wouldn’t be able to work the whole time, I brought a copy of a great paper on Traits. The paper got me thinking a lot about dojo.declare() and dojo.delegate().

Today, Dojo’s delegate() function is a straightforward implementation of the Boodman/Crockford delegation pattern which Doug calls “beget” and which ES 3.1 will refer to as Object.create:

dojo.delegate = (function(){
    // boodman/crockford delegation w/ cornford optimization
    function TMP(){};
    return function(obj, props){
        TMP.prototype = obj;
        var tmp = new TMP();
        if(props){
            dojo._mixin(tmp, props);
        }
        return tmp; // Object
    }
})();

This function returns a new object which looks to the old object for things it does not itself have. Imagine an object foo which contains pithy truisms:

var foo = {
  science: "rocks!",
  learning: "is how you know you're alive"
};

We now want to promigulate our opinions, so we can delegate the responsibility of forming them:

var bar = dojo.delegate(foo, {
  testify: function(){
    console.debug("science ", this.science, "and learning", this.learning);
  }
);

Now, our bar object can change its mind independently of foo, but until it does, it’ll behave as though foo‘s views are its own:

bar.testify(); // outputs: "science rocks and learning is how you know you're alive"

// bar refines its opinion
bar.science = "is a process"; 
bar.learning = "requires humility";
foo.science == "rocks!"; // still true

bar.testify(); // outputs: "science is a process and learning requires humility"

But what about when the chain gets deeper? The fact that bar can’t “see” foo‘s values via this isn’t much of a problem when the hierarchy isn’t very long, but if you’re specializing a behavior or complex interaction, making it possible to get at the parent’s values for properties and methods becomes more pressing.

Neil has previously written about lightweight subclassing, but for as good as it its, it doesn’t get us all the way there either. In regular OO-style languages, the inheritance system gives you an out via a “super” keyword or convention. This type of property shadowing-with-exceptions is a huge boon to composition in class-based languages, but it’s not the whole story. Indeed, the Traits paper was all about the shortcomings of this special-purpose mechanism. What we want for both long delegation chains and long inheritance hierarchies is a more general system; in essence a way to say “I want to control how things are shadowed and which ones an item points at in each level of the hierarchy”.

What if we could make delegate() savvy of this type of indirection? Here’s my quick prototype:

delegate = (function(){
    var tobj = {};
    var TMP = function(){};
    return function(obj, props){
        TMP.prototype = obj;
        var tmp = new TMP();
        if(props){
            var remaps = props["->"];
            if(remaps){
                delete props["->"];
                for(var x in remaps){
                    if(tobj[x] === undefined || tobj[x] != remaps[x]){
                        if(remaps[x] == null){
                            // support hiding via null assignment
                            tmp[x] = null;
                        }else{
                            // alias the local version away
                            tmp[remaps[x]] = obj[x];
                        }
                    }
                }
            }
            dojo.mixin(tmp, props);
        }
        return tmp; // Object
    }
})();

This new version of delegate() accepts a specially named “->” property in the list of items to add to the destination object. Items in this list can either “shadow null” (hide entirely) the parent’s property or can provide a new name for it, assuming of course that the new object will also have a property of that name. Here’s a quick example of “->” at work with our previous example. This time, foo also has a “testify” method that we’d like bar to be able to control without having to copy the implementation:

var foo = {
    science: "rocks!",
    learning: "is how you know you're alive",
    testify: function(){
        console.debug("science ", this.science, "and learning", this.learning);
    }
};

var bar = delegate(foo, {
    "->": {
        "testify": "grampsSays" // maps foo's "testify" to bar's "grampsSays"
    },
    testify: function(){
        if(this.science && this.learning){
            this.grampsSays(); // call the re-named "testify"
        }else{
            console.debug("this object is strikingly ignorant");
        }
    },
});

bar.testify(); // outputs: "science rocks and learning is how you know you're alive"
bar.science = false;
bar.testify(); // outputs: "this object is strikingly ignorant"

That New Object Smell

The last missing piece of the hierarchy pie here is that there’s no initializer for the objects which come from a delegation. A simple addition of some property detection code to look for an initializer can easily handle that:

delegate = (function(){
    var tobj = {};
    var TMP = function(){};
    return function(obj, props){
        // boodman/crockford delegation w/ cornford optimization. 

        TMP.prototype = obj;
        var tmp = new TMP();
        if(props){
            var remaps = props["->"];
            if(remaps){
                delete props["->"];
                // like dojo.mixin(), except w/o key/key mapping
                for(var x in remaps){
                    // "safe" copy properties
                    if(tobj[x] === undefined || tobj[x] != remaps[x]){
                        if(remaps[x] == null){
                            // support hiding via null assignment
                            tmp[x] = null;
                        }else{
                            // alias the local version away
                            tmp[remaps[x]] = obj[x];
                        }
                    }
                }
            }
            dojo.mixin(tmp, props);
        }

        // support for "constructor" functions. The name "init" is arbitrary.
        if(typeof tmp["init"] == "function"){
            tmp.init.call(tmp);
        }

        return tmp; // Object
    }
})();

And there we have it. A style of delegation that easily supports both Trait-like name aliasing (and null shadowing) as well as internal initializers. Since our upgraded delegate can handle nulling out a parent’s value for a property, we also have a straightforward way to prevent parent initializers from being called (or being called/chained – at our discretion – by a new name):

var foo = {
    science: "rocks!",
    learning: "is how you know you're alive",
    testify: function(){
        console.debug("science ", this.science, "and learning", this.learning);
    }
};

var bar = delegate(foo, {
    init: function(){ this.testify(); }
});
// outputs: "science rocks and learning is how you know you're alive"

var baz = delegate(bar, {
    // map away the parent's constructor
    "->": {
        "init": "superInit"
    },
    // provide our own constructor
    init: function(){
        console.debug("howdy!");
        this.superInit(); // call the super-object ctor
    }
});
// outputs: "howdy", "science rocks and learning is how you know you're alive"

var thud = delegate(baz, {
    "->": { "init": null } // hide the parent ctor
});
// outputs: nothing

This form of delegate is likely to appear in Dojo 1.3 along with similar improvements to dojo.declare() to help alleviate the composition problems associated with using complex sets of mixins.

Update: corrected the null-out branch and updated the text with Doug’s note that beget/delegate will be called Object.create() in 3.1.

8 Comments

  1. Posted October 10, 2008 at 11:20 pm | Permalink

    Out of curiosity, do you have any direct references to when boodman/crockford discovered that trick? MochiKit.Base.clone does the same thing, I had never heard of theirs before just now! :)

    MochiKit’s implementation even does it in a different way entirely, by (ab)using arguments.callee to get the clone function itself and mangle its prototype property. This is not re-entrant or thread-safe but JS doesn’t have interrupts or threads so I never bothered to create a “safer” version :)

  2. Posted October 10, 2008 at 11:41 pm | Permalink

    You might want to check out the .NET concept of delegate before you commit to that name. This could get pretty confusing to many people no matter how useful the concept is.

  3. Posted October 11, 2008 at 6:22 am | Permalink

    Object.create(prototype) will be in the next edition of ECMAScript. It begets.

  4. Posted October 11, 2008 at 7:32 am | Permalink

    Bertrand:

    We knew of the .NET terminology, but we’ve been using “delegate()” in this way in Dojo for a long while. The method/object tuple thing is generally called “bind()” in most JS toolkits (we call it hitch()) and the next version of the language will call it ‘create()’. Obviously we don’t want to confuse anyone, but frankly this hasn’t come up before (to the best of my knowledge). Perhaps Dojo folks just haven’t been aware of delegate()’s existance? Either way, if it becomes a real problem, we’ll give it a new alias.

    Bob:

    The earliest I knew of it was Aaron Boodman sketching it out on a napkin for me at a bar in the Mission 4 years ago. I think Doug noted the pattern somewhat after that, but the exact timing is fuzzy. I do know that both discovered/noted it independently around the same time.

    Regards

  5. Posted October 11, 2008 at 12:35 pm | Permalink

    By the way, one of my new favorite delegation patterns that I like to use to change properties that are accessed during construction, e.g. templatePath in Dijit widgets:

    var widget = dojo.delegate(dijit.form.Button.prototype);
    widget.templatePath = dojo.moduleUrl(“some.other”, “template.html”);
    dijit.form.Button.call(widget, {});

  6. Posted October 11, 2008 at 8:24 pm | Permalink

    In the foo, bar, baz, thud example with aliasing super init methods: any thoughts on schemes on generic aliasing schemes?

    Say thud wanted to define init — it cannot alias baz’s init to superInit, since baz already has that name taken.

    What I did before was to name it the name of the parent object, so thud aliases init to bazInit, baz aliases init to barInit, etc…

    I did not like it because of the dependency on object name, something mutable. It worked out because usually the names did not change. Just curious if there is a better way.

    Anyway, interesting stuff. I wonder if aliasing to null should mean delegate deletes the property. So duck type checking like:

    if (“testify” in bar)

    would return false if bar did a -> testify: null. In that case, maybe make the alias convention should be testify: undefined?

  7. Posted October 12, 2008 at 8:14 pm | Permalink

    It overlaps with dojox.lang.mix somewhat (added back in September 2008) that sketches out a generic rename/remove functionality. It allows to subset (remove in-place, or copy selectively while mixing) and rename attributes to your liking. There is a simple in-place version for slicing and dicing objects, and more general copying mixing version that supports a concept of filters and decorators for custom mixing algorithms. A bunch of decorators are predefined in the same file. Check it out.

    In general I support more generic facilities for OO in Dojo, and I like your lightweight approach.

    Speaking about being lightweight: Neil Roberts made an observation long time ago that the for..in loop is inefficient in general (e.g., going over prototypes) and replacing with equivalent assignments can speed things up especially while number of mixed properties is small. It would be nice to have an optional way to bypass the loop in time-critical cases.

  8. Posted October 12, 2008 at 8:15 pm | Permalink

    Huh, actually dojox.lang.mix was added in August 2008, sorry.