Links

pmuellr is Patrick Mueller

other pmuellr thangs: home page, twitter, flickr, github

Friday, June 04, 2010

bind() considered harmful

A recent passive-aggressive twitter message from me:

Oh yeah. Keep on bind()'ing folks, because bind() rocks. http://bit.ly/aPyfuV

In case you can't tell, the message is facetious. bind() does not actually rock.

what is bind()?

bind() is a JavaScript function implemented by many JavaScript libraries and included in ECMAScript 5. If you aren't already familiar with bind(), this post isn't going to make much sense, but if you're curious anyway, see Prototype's documentation for their bind().

Let's look at what bind() is actually doing:

  • associates an object to use as this (aka the receiver) when the function is invoked
  • associates additional parameters to be passed to the function when the function is invoked (aka currying).

bind() does this by returning a new function, which when invoked, arranges for the object you want as the receiver and the additional parameters to be passed to the original function. It hides these values in the newly created function, associated with the closure the function was returned from.

bind() is frequently used for callbacks and event handlers, which typically allow you to pass a function as the callback, but don't provide a way to specify the receiver or additional parameters for the actual call to the callback.

why does bind() not rock?

  • gets in the way of debugging

    Take a look at the bug I linked to in the Twitter message above: Bug 40080: Web Inspector: better Function.prototype.bind for the internal code

    The bug concerns providing a better debug story around functions that have been bind()'d. Because the current story isn't very pretty. When you stumble upon a bind()'d function in the debugger, you see the source for the bind() function itself, which isn't what you want to see. In addition the resulting bound function is usually anonymous, which means your stack traces, profile reports, etc, will be filled with (anonymous function) entries.

  • garbage

    Calling bind() creates a new function object every time it's called. Not just a string, or empty object, or array. A function. Which I'm guessing is more expensive than simpler objects. [yes, I should do some measurements.] A closure is also created and associated with the function (which is where the receiver and curried arguments are stored). More garbage.

    Now imagine that, for whatever reason, you need to add and then remove a callback frequently, over and over again, for some reason. Or set a timer over and over again. And you're using bind(). Think of the garbage you're creating.

  • Alex Russell posted some thoughts on bind() in a post to the es-discuss mailing list (item 3).

    So why does this [using bind()] suck? Two reasons: it's long-ish to type, and it doesn't do what the dot operator does -- i.e., return the same function object every time.

    It is longer, and therefore yuckier. It's also often not DRY (obj referenced twice):

        setTimeout( obj.method.bind(obj, "John"), 100 );
    

    The second point is explained by Alex in his post:

    Many functions, both in the DOM and in the library, accept functions as arguments. ES5 provides a bind() method that can ease the use of these functions when the passed method comes from an object. This is, however, inconvenient. E.g.:
       node.addEventListener("click", obj.method.bind(obj, ...));
    
    Also, insufficient. Event listeners can't be detached when using Function.prototype.bind:
       // doesn't do what it looks like it should
       node.removeEventListener("click", obj.method.bind(obj, ...));
    

    So the trick here, if you want to use removeEventListener(), is that you have to store the result of a single call to bind() and then use that value in subsequent addEventListener() / removeEventListener() paired calls. Alex's post suggests a new language feature to work around this (btw, I'm not in favor of his proposed language feature).

why are we in a bind with bind() today?

It's pretty obvious to see how we got to the point where you need to use bind() in your code today.

Historically, JavaScript was a glue language that let you do a light amount of programmatic processing against stuff in your page. When specifying a callback/listener, you didn't have to worry so much about the receiver of the function you passed in; you were probably using global variables instead of creating your own little objects.

And so the places that take callbacks, like setTimeout(), onload handlers, etc, didn't really have a need for you to specify the receiver of of the callback when it was invoked. The receiver was always ... well, whatever it was for your callback.

Fast-forward a decade, and now we have people building huge systems out of JavaScript, using some sort of "class" story, or living the hippy prototype lifestyle, or who knows what kids are doing today. In any case, there's often an "object" in the picture, and you'd often like to arrange to have that object be the receiver of the callback. Quite often, you'd like for the callback function to be a method of an object, and have the receiver of the callback be that object.

The problem is, there's no where to put the receiver; all the pre-existing callback patterns just allowed the use of a function parameter. The trick with bind() is that it attaches the receiver, and possibly curry'd arguments, to an invisible bag wrapped around a newly created function which is a delegated version of your callback function. Nature will find a way.

how can we fix the evils of bind()?

For me, the root of the problem is that we're passing the method receiver in a secondary channel, the bound function. So, stop doing that. Pass it explicitly.

Let's play with changing the addEventListener() function to accommodate a new receiver parameter. Here's the current function signature:

    target.addEventListener(type, listener, useCapture)

We can add the receiver parameter to the end of the function:

    target.addEventListener(type, listener, useCapture, receiver)

or we could allow listener and receiver to be combined together in an array and used where the existing listener value is today:

    target.addEventListener(type, [receiver, listener], useCapture)

This second flavor tastes better to me.

what does the fix smell like?

Let's compare the code. For the examples below, obj is the receiver of the callback, callback() is a method available on the obj object.

Using ECMAScript 5's bind() method:

    node.addEventListener("click", obj.callback.bind(obj))

Here's the four-arg addEventListener():

    node.addEventListener("click", obj.callback, false, obj)

Here's the two-element-array-listener addEventListener():

    node.addEventListener("click", [obj, obj.callback])

These examples could be made even DRYer, if instead of passing a function reference, you pass a string, which will be used as a property name to obtain the function from the receiver object:

    node.addEventListener("click", "callback", false, obj)
    node.addEventListener("click", [obj, "callback"])

In terms of being able to handle the removeEventListener() case as well, when using the ECMAScript 5 version of bind() you would have to arrange to store a copy of the bound function, so you can send the exact same function both addEventListener() and removeEventListener(). My proposed versions could do a compare against the parameters or array elements, allowing you to use the exact same parameters on addEventListener() and removeEventListener().

In other words, here's how you do it in ECMAScript 5:

    var boundFunction = obj.callback.bind(obj)
    
    node.addEventListener("click", boundFunction)
    ...
    node.removeEventListener("click", boundFunction)

And here's how you do it with my proposal:

    node.addEventListener("click", [obj, "callback"])
    ...
    node.removeEventListener("click", [obj, "callback"])

This invocation pattern works the same for the other form of call that I proposed.

Curried arguments can be handled the same way the receiver parameter is handled; passed as additional arguments to addEventListener() (not needed for removeEventListener()?), or an additional element in the array where the listener argument was previously used. One simplification would be to allow a single curried argument - other callback systems typically refer to this as userData or clientData - rather than deal with a variadic list. It's simple enough to combine multiple curried arguments into an object or array for use as a userData argument.

actually, bind() isn't always evil

Although I've spent this entire post complaining about bind(), I will acknowledge it's power and usefulness. Particularly in meta-programming and function programming.

My primary complaint is having to use bind() is something pedestrian as callbacks. That's too much.

the obligatory Smalltalk reference

My views on this subject are biased by my exposure to OTI/IBM Smalltalk. Read up on the Callbacks section on page 150 of "IBM Smalltalk: Programmer's Reference".

You'll note that my suggestion here is no different than the addCallback:receiver:selector:clientData: method described in that manual, just a shorter name.

Wednesday, June 02, 2010

coopitition

A neat thing happened last month. I entered a feature request against the Web Inspector debugger shipped with WebKit, to get some new function added. Two weeks later, that function was implemented in the FireBug debugger for FireFox. Coincidence?

The feature request was this one: Bug 38664: Web Inspector: add a "table" method to console, to allow output of tabular data. I got the idea for this from looking at another Web Inspector bug: Bug 30897: Web Inspector: Support Debugging via HTTP Headers like FirePHP and FireLogger. Turns out that FirePHP (another interesting project) has had support for tabular console output for a while. I immediately felt left out.

So how did the FireBug folks beat the Web Inspector folks (me included) to the implementation? Turns out that IBM employs folks who work on both Web Inspector and FireBug. My primary contact for FireBug stuff in IBM is John J Barton, and I thought this new function would be interesting enough they'd want it too. I guess I was right. John and I trade notes on stuff like this from time to time.

Once Jan Odvarko (aka "Honza") posted his blog entry Tabular logs in Firebug, my interest in this bug grew, for some reason :-) So, had some conversation with Jan, on the bug, last Friday. It was great for Jan to provide additional feedback in WebKit's Bugzilla. And then I was totally surprised yesterday morning to find that a bunch of additional great ideas around the feature request got posted back to the bug over the weekend, from Jan and other Web Inspector developers.

I decided to try to summarize where we were, provide some examples, and ended up writing a little console.table() simulator to play with, all of which is currently available in this file: console.table() proposal. Please post comments on that file back to the bug, for now.

I love friendly competition!