Interlude: Greenspun’s Tenth Rule

London Interlude26

In Metaobjects and “self”,27 we just saw how extendPrivately breaks methods that need to refer to the receiver, because it swaps the method invocation context for an object that encodes private state. It’s possible to “fix” the problem by separating private state from “self,” emulating the way languages like Ruby separate private instance variables from self, and we demonstrated one way to do that, privateExtendWithSelf:

function privateExtendWithSelf (receiver, mixin) {
  var methodName,
      privateProperty = Object.create(null);
      
  privateProperty.self = receiver;

  for (methodName in mixin) {
    if (mixin.hasOwnProperty(methodName)) {
      receiver[methodName] = mixin[methodName].bind(privateProperty);
    };
  };
  return receiver;
};

var Container = {
  initialize: function () {
    this._contents = [];
    return this.self;
  },
  add: function (something) {
    this._contents.push(something);
  },
  size: function () {
    return this._contents.length;
  },
  description: function () {
    return "I'm a bag with " + this.self.size() + " things in it."
  }
}

var bag = privateExtendWithSelf({}, Container).initialize();

bag.add("something");

bag.description();
  //=> "I'm a bag with 1 things in it."

However, thsi is not a cure-all.

a problem with “explicit self”

In JavaScript Allongé, we find the fluent decorator. One version of it looks like this:

function fluent (methodBody) {
  return function () {
    var value = methodBody.apply(this, arguments);
    
    return value === undefined
           ? this
           : value;
  }
}

Under normal circumstances, we use it to make methods “fluent,” meaning that they return the receiver to facilitate method chaining. In the version above, if we have a method that doesn’t explicitly return a value, the fluent decorator has it return this.

Let’s combine fluent with privateExtendWithSelf:

var Container = {
  initialize: fluent(function () {
    this._contents = [];
  }),
  add: fluent(function (something) {
    this._contents.push(something);
  }),
  size: fluent(function () {
    return this._contents.length;
  }),
  description: fluent(function () {
    return "I'm a bag with " + this.self.size() + " things in it."
  })
}

var bag = privateExtendWithSelf({}, Container).initialize();

bag.add("something");
  //=> TypeError: Object object has no method 'add'

You have spotted the problem, but at four in the morning when dealing with an urgent and critical bug, it may escape our notice that fluent doesn’t play nicely with privateExtendWithSelf. Or, for that matter, with extendPrivately. Or any other scheme that involves separating context from self-hood.

In conversation, we say that “fluent modifies a method to return the receiver,” but in JavaScript that isn’t correct. fluent modifies a function to return its context. In Vanilla JavaScript, when a function is invoked as a method, that is the receiver by default. But it isn’t the receiver if we’ve used Function.prototype.bind, it isn’t the receiver if we invoke the function as an ordinary function, and it isn’t the receiver if we use .call or .apply to invoke the function.

We can, of course, write our own parallel decorator:

function selfie (methodBody) {
  return function () {
    var value = methodBody.apply(this, arguments);
    
    return value === undefined
           ? (this.self || this)
           : value;
  }
}

var Container = {
  initialize: selfie(function () {
    this._contents = [];
  }),
  add: selfie(function (something) {
    this._contents.push(something);
  }),
  size: selfie(function () {
    return this._contents.length;
  }),
  description: selfie(function () {
    return "I'm a bag with " + this.self.size() + " things in it."
  })
}

var bag = privateExtendWithSelf({}, Container).initialize();

bag.add("something");

bag.description();
  //=> "I'm a bag with 1 things in it."

a worrying problem

selfie works, but there is a design smell: Why do extendPrivately and privateExtendWithSelf not work seamlessly with other tools we develop for working with objects, functions, and methods?

One of the things we seek in developing tools is the ability to mix them and match them, to maximize interoperability. This is exactly why we try to have private data in the first place, to prevent code from becoming coupled and ossifying our code base.

However, when we develop tools that have their own idiosyncratic conventions (such as separating context from self), we create little “islands of incompatibility” between our tools. We then have to modify our tools to interoperate (as we did with fluent and selfie), or give up on the flexibility of working with many small and simple elements that compose together.

There is rarely an obvious, correct solution to this problem. Imagine, for the sake of argument, that JavaScript was invented in 1985 instead of 1995. OOP was very new and not popular, so Brendan Eich could easily have decided to create a mixed functional-procedural language without any support for the new keyword, prototypes, or method dispatch.

Then in 1995, JavaScript could have been upgraded to include the OO features. Now programmers would have the exact same disconnect: Tools that worked perfectly well for functions would break for methods. For example:

function before1 (a, b) {
  return function (c) {
    a(c);
    return b(c);
  }
}

before1 takes two functions, and returns a function that evaluates one befor ereturning the result of the other. But it doesn’t work with methods, because it ignores the context. In 1995, we’d have had to rewrite it to read:

function before2 (a, b) {
  return function (c) {
    a.call(this, c);
    return b.call(this, c);
  }
}

Then in 2015, we’d rewrite it again to read:

function before3 (a, b) {
  return function (c) {
    a.call(this.self || this, c);
    return b.call(this.self || this, c);
  }
}

it’s philip greenspun’s fault

Not really! But he did coin an aphorism that is very applicable:

Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.–Greesnpu’s Tenth Rule

We wren’t writing C or Fortran, but the general idea applies that given a language with relatively sparse facilities for abstraction, the only way to write large-scale and/or complex software is to build our own facilities for abstraction. We can’t just program, we have to meta-program. And this eventually reaches the point where we’ve created our own programming langauge on top of our existing programming language.

As a Lisper, he naturally equated this with recreating half of Common Lisp, because Common Lisp at the time had the most sophisticated facilities for large-scale abstractions. Thus, any attempt to create large-scale sophistciated abstractiions was destined to create equivalents for the features that were already built into Common Lisp, only without the rigor and reliability of Common Lisp.

Today, the verb “Greenspunning” refers to attempts to emulate features of a more “powerful” programming language in a less powerful language. Of course, programming is always about constructing abstractions, so not all abstractions are “Greenspunning.” While the definition isn’t 100% precise, one of the key symptoms of Greenspunning is when the resulting feature has some kind of non-local effect.

For example, Node.js is built around a standard library full of functions that expect a callback as their last parameter. This has non-local effects, because you must organize your emtire program around callbacks. A Lisper might say that they have reinvented continuations and are demanding that programmers hand-transpile their programs in Continuation-Passing Style.

When we build upon JavaScript’s facilities for abstraction to create new and more powerful facilities, we are often Greenspunning. This is not ncessarily a bad thing, the trade-off between the advantages of the abstraction and the disadvantages of its incompatibility with Vanilla JavaScript may be worthwhile.

Within the context of this book, it is always valuable to think through what is possible and what tradeoffs are involved. This is because inevitably, our large-scale JavaScript programs will contain “half of Common Lisp,” whether we want them to or not.

So given that our programs will grow and inevitably contain more facilities for abstraction than are “baked into” Vanilla JavaScript, it behooves us to deeply understand what choices are possible, and to be cognizant of the implications and tradeoffs inherent in those choices.