The Big Idea - JavaScript Spessore: A thick shot of objects, metaobjects, & protocols (2014)

JavaScript Spessore: A thick shot of objects, metaobjects, & protocols (2014)

The Big Idea

3

Detail of a Mazzer Mini espresso grinder. The grind is at least as important as the press, yet most people spend more time and money on their espresso machine than they do on their grinder.

In This Chapter

In this chapter, we propose that JavaScript’s “Big Idea” is that you can use functions to transform and compose primitives, functions, and objects. We will suggest that programming with objects is not separate and distinct from programming with functions, but simply another way to work with JavaScript’s big idea.

This thinking will set the stage for the object-oriented programming techniques we’ll explore throughout the book.

Is JavaScript Functional or Object-Oriented?

One of JavaScript’s defining characteristics is its treatment of functions as first-class values. Like numbers, strings, and other kinds of objects, references to functions can be passed as arguments to functions, returned as the result from functions, bound to variables, and generally treated like any other value.4

Here’s an example of passing a reference to a function around. This simple array-backed stack has an undo function. It works by creating a function representing the action of undoing the last update, and then pushing that onto a stack of actions to be undone:

var stack = {

array: [],

undoStack: [],

push: function (value) {

this.undoStack.push(function () {

this.array.pop();

});

return this.array.push(value);

},

pop: function () {

var popped = this.array.pop();

this.undoStack.push(function () {

this.array.push(popped);

});

return popped;

},

isEmpty: function () {

return array.length === 0;

},

undo: function () {

this.undoStack.pop().call(this);

}

};

stack.push('hello');

stack.push('there');

stack.push('javascript');

stack.undo();

stack.undo();

stack.pop();

//=> 'hello'

Functions-as-values is a powerful idea. And people often look at the idea of functions-as-values and think, “Oh, JavaScript is a functional programming language.” No.

information

In computer science, functional programming is a programming paradigm, a style of building the structure and elements of computer programs, that treats computation as the evaluation of mathematical functions and avoids state and mutable data.

Wikipedia

Functional programming might have meant “functions as first-class values” in the 1960s when Lisp was young. But time marches on, and we must march alongside it. JavaScript does not avoid state, and JavaScript embraces mutable data, so JavaScript does not value “functional programming.”

Handshake, Glider, Boat, Box, R-Pentomino, Loaf, Beehive, and Clock by Ben Sisko

objects

JavaScript’s other characteristic is its support for objects. Although JavaScript’s features seem paltry compared to rich OO languages like Scala, its extreme minimalism means that you can actually build almost any OO paradigm up from basic pieces.

Now, people often hear the word “objects” and think kingdom of nouns. But objects are not necessarily nouns, or at least, not models for obvious, tangible entities in the real world.

One example concerns state machines. We could implement a cell in Conway’s Game of Life using if statements and a boolean property to determine whether the cell was alive or dead:5

var __slice = [].slice;

function extend () {

var consumer = arguments[0],

providers = __slice.call(arguments, 1),

key,

i,

provider;

for (i = 0; i < providers.length; ++i) {

provider = providers[i];

for (key in provider) {

if (provider.hasOwnProperty(key)) {

consumer[key] = provider[key];

};

};

};

return consumer;

};

var Universe = {

// ...

numberOfNeighbours: function (location) {

// ...

}

};

var Alive = 'alive',

Dead = 'dead';

var Cell = {

numberOfNeighbours: function () {

return Universe.numberOfNeighbours(this.location);

},

stateInNextGeneration: function () {

if (this.state === Alive) {

return (this.numberOfNeighbours() === 3)

? Alive

: Dead;

}

else {

return (this.numberOfNeighbours() === 2 || this.numberOfNeighbours() === 3)

? Alive

: Dead;

}

}

};

var someCell = extend({

state: Alive,

location: {x: -15, y: 12}

}, Cell);

You could say that the “state” of the cell is represented by the primitive value 'alive' for alive, or 'dead' for dead. But that isn’t modeling the state in any way, that’s just a name. The true state of the object is implicit in the object’s behaviour, not explicit in the value of the .state property.

Here’s a design where we make the state explicit instead of implicit:

function delegateToOwn (receiver, propertyName, methods) {

var temporaryMetaobject;

if (methods == null) {

temporaryMetaobject = receiver[propertyName];

methods = Object.keys(temporaryMetaobject).filter(function (methodName) {

return typeof(temporaryMetaobject[methodName]) === 'function';

});

}

methods.forEach(function (methodName) {

receiver[methodName] = function () {

var metaobject = receiver[propertyName];

return metaobject[methodName].apply(receiver, arguments);

};

});

return receiver;

};

var Alive = {

alive: function () {

return true;

},

stateInNextGeneration: function () {

return (this.numberOfNeighbours() === 3)

? Alive

: Dead;

}

};

var Dead = {

alive: function () {

return false;

},

stateInNextGeneration: function () {

return (this.numberOfNeighbours() === 2 || this.numberOfNeighbours() === 3)

? Alive

: Dead;

}

};

var Cell = {

numberOfNeighbours: function () {

return thisGame.numberOfNeighbours(this.location);

}

}

delegateToOwn(Cell, 'state', ['alive', 'aliveInNextGeneration']);

var someCell = extend({

state: Alive,

location: {x: -15, y: 12}

}, Cell);

In this design, delegateToOwn delegates the methods .alive and .stateInNextGeneration to whatever object is the value of a Cell’s state property.

So when we write someCell.state = Alive, then the Alive object will handle someCell.alive and someCell.aliveInNextGeneration. And when we write someCell.state = Dead, then the Dead object will handle someCell.alive and someCell.aliveInNextGeneration.

Now we’ve taken the implicit states of being alive or dead and transformed them into the first-class values Alive and Dead. Not a string that is used implicitly in some other code, but all of “The stuff that matters about aliveness and deadness.”

This is not different than the example of passing functions around: They’re both the same thing, taking something would be implicit in another design and/or another language, and making it explicit, making it a value. And making the whole thing a value, not just a boolean or a string, the complete entity.

This example is the same thing as the example of a stack that handles undo with a stack of functions: Behaviour is treated as a first-class value, whether it be a single function or an object with multiple methods.

JavaScript Algebras

As we saw in Is JavaScript Functional or Object-Oriented?, functions are excellent for representing single-purposed units of behaviour such as an action to undo. Objects can obviously model nouns in the domain, but they can also model states and other more complex manifestations of behaviour.

Functions have another role to play in object-oriented programs, one that lies outside of domain behaviour. Functions form the basis for an algebra of values.

Consider these functions, begin1 and begin. They’re handy for writing function advice, for creating sequences of functions to be evaluated for side effects, and for resolving method conflicts when composing behaviour:

var __slice = [].slice;

function begin1 () {

var fns = __slice.call(arguments, 0);

return function () {

var args = arguments,

values = fns.map(function (fn) {

return fn.apply(this, args);

}, this),

concretes = values.filter(function (value) {

return value !== void 0;

});

if (concretes.length > 0) {

return concretes[0];

}

}

}

function begin () {

var fns = __slice.call(arguments, 0);

return function () {

var args = arguments,

values = fns.map(function (fn) {

return fn.apply(this, args);

}, this),

concretes = values.filter(function (value) {

return value !== void 0;

});

if (concretes.length > 0) {

return concretes[concretes.length - 1];

}

}

}

Both begin1 and begin create a function by composing two or more other functions. Composition is the primary activity in constructing programs: Making a complex piece of behaviour out of smaller, simpler pieces of behaviour.

In a sense, all programming is composition. However, when we use functions to compose functions, we deliberately restrict the way in which the functions are invoked in relation to each other. This makes it easier to understand what is happening, because the functions we use for composition–like begin1 and begin–have very simple and well-defined ways of constructing new functions from existing functions.

Another valuable use for functions is to transform other functions. The Underscore library includes a few, as does allong.es. Here’s one of the simplest:

function not (fn) {

return function () {

return !fn.apply(this, arguments);

}

}

not takes a function and return a function that is the logical inverse. When fn(...) returns a truthy value, not(fn)(...) returns false. When fn(...) returns a falsey value, not(fn)(...) returns true.

Functions that compose and transform other functions allow us to build functions out of smaller, interchangeable components in a structured way. This is called creating an algebra of functions: A set of operations for creating, composing, and transforming functions.

an algebra of values

Functions that compose and transform functions into other functions are very powerful, but things do not stop there.

It’s obvious that functions can take objects as arguments and return objects. Functions (or methods) that take an object representing a client and return an object representing and account balance are a necessary and important part of software.

But just as we created an algebra of functions, we can create an algebra of objects. Meaning, we can write functions that take objects and return other objects that represent a transformation of their arguments.

Here’s a function that transforms an object into a proxy for that object:

function proxy (baseObject, optionalPrototype) {

var proxyObject = Object.create(optionalPrototype || null),

methodName;

for (methodName in baseObject) {

if (typeof(baseObject[methodName]) === 'function') {

(function (methodName) {

proxyObject[methodName] = function () {

var result = baseObject[methodName].apply(baseObject, arguments);

return (result === baseObject)

? proxyObject

: result;

}

})(methodName);

}

}

return proxyObject;

}

Have you ever wanted to make an object’s properties private while making its methods public? You wanted a proxy for the object:

var stackWithPrivateState = proxy(stack);

stack.array

//=> []

stackWithPrivateState.array

//=> undefined

stackWithPrivateState.push('hello');

stackWithPrivateState.push('there');

stackWithPrivateState.push('javascript');

stackWithPrivateState.undo();

stackWithPrivateState.undo();

stackWithPrivateState.pop();

//=> 'hello'

The proxy function transforms an object into another object with a similar purpose. Functions can compose objects as well, here’s one of the simplest examples:

var __slice = [].slice;

function meld () {

var melded = {},

providers = __slice.call(arguments, 0),

key,

i,

provider,

except;

for (i = 0; i < providers.length; ++i) {

provider = providers[i];

for (key in provider) {

if (provider.hasOwnProperty(key)) {

melded[key] = provider[key];

};

};

};

return melded;

};

var Person = {

fullName: function () {

return this.firstName + " " + this.lastName;

},

rename: function (first, last) {

this.firstName = first;

this.lastName = last;

return this;

}

};

var HasCareer = {

career: function () {

return this.chosenCareer;

},

setCareer: function (career) {

this.chosenCareer = career;

return this;

},

describe: function () {

return this.fullName() + " is a " + this.chosenCareer;

}

};

var PersonWithCareer = meld(Person, HasCareer);

//=>

{ fullName: [Function],

rename: [Function],

career: [Function],

setCareer: [Function],

describe: [Function] }

Functions that transform objects or compose objects act at a higher level than functions that query objects or update objects. They form an algebra that allows us to build objects by transformation and composition, just as we can use functions like begin to build functions by composition.

JavaScript treats functions and objects as first-class values. And the power arising from this is the ability to write functions that transform and compose first-class values, creating an algebra of values.

Composeabilitity

As a language, JavaScript is agnostic about architecture. It does not have Java’s heavyweight class hierarchy. It does not have Smalltalk’s UI framework. Thus, it is possible to build your own monolithic architecture that dictates the API of all objects that fit into it and wires them together.

It’s also possible to design around lots of smaller componets that “compose.” This is the principle behind JavaScript Algebras, to create collections of functions or objects and to write operations that compose new itesm from existing items.

We’ll see those ideas play out in coming sections. For example, Predicate Dispatch is one way to compose new functions from smaller functions that each handle one “case” for an algorithm. Encapsulation and Composition of Metaobjects composes metaobjects (like prototypes) together our of smaller, independent metaobjects with focused responsibilities.

The key to making composeability work well is always to have parts that are as independent of each other as possible. This makes combining and recombining them easy and practical.

While heavyweight OO is a valid approach, our focus in this book will be on lightweight components that compose, an approach that plays to JavaScript’s strengths and makes it easy to pick as few or as many of the techniques we discuss for a particular project.