Where Array#Extras fall short

Arrays come with many built-in functions. But if something is missing, extending them is hard

Author's image
Tamás Sallai
3 mins

Array with extras

Arrays already have map, filter, reduce and some other functions. The example from the first post can then be simplified to (Try it):

const array = [5, 10, 15];

const result = array
	.map((i) => i + 3)
	.filter((i) => i % 2 === 0);
console.log(result); // [8, 18]

This is a clear, concise solution, without lingering variables or awkward syntax.

The problem is that Arrays are hard to extend with new functions. And this is a serious shortcoming, as having some functions does not mean that you'll never need anything else.

What if you need head, that returns the first element?

Extending the prototype

A simple solution is to add the function to the Array prototype, amending all Arrays at once (Try it):

Array.prototype.head = function () {
	return this[0];
}
console.log(result.head()); // 8

But this is a polluting operation. When this code runs, it has far-reaching effects, modifying all Arrays.

It can be a problem in multiple ways. What if different people want to define the function in different ways? Even in this simple example, should head throw an Error or return undefined for an empty Array? Also, were libraries to follow this practice, that would create a mess on a whole new level.

Moreover, if the function is defined just before usage, it is a side-effect. If it is defined globally, that adds to the surprises people not familiar with the codebase experience. These surprises are what makes onboarding progressively harder.

Consider the following scenario (Try it):

// Can not use [].head() here
function1(); // defines Array.prototype.head()
// [].head() version 1
function2(); // redefines head()
// [].head() version 2

If you decide to get rid of the function1 call, code that comes after it that also relies on the first version will break.

Undo pollution

A possible approach is to undo the pollution after the function is called (Try it):

const oldhead = Array.prototype.head;
try {
	...
}finally {
	Array.prototype.head = oldhead;
}

This might work in some situations but is flawed in several ways.

First, it is ugly. Wrapping everything in a try...finally just to avoid side effects is a clear sign that we're doing something wrong here.

But also, it has more localized problems. What if there is a function call that uses head? In that case, depending on the caller, the function may use different versions (Try it):

const func = () => {
	console.log(Array.prototype.head);
}
func(); // undefined
...
const oldhead = Array.prototype.head;
try {
	func(); // function
}finally {
	Array.prototype.head = oldhead;
}

Conclusion

When all you need to use is present in the Array prototype, use that. It provides the cleanest collection processing you can ever have. But this approach falls short when something is missing.

The lazy solution is to cut corners and augment the prototype, but this approach comes with long-term costs hardly justified by the benefits.

But don't worry, there are other solutions to this problem. We'll look into them in the next posts.

December 5, 2017
In this article