The case for Array#replace()

Overriding an array without intermediate variables

There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors. Therefore, if we can reduce the amount of time we spend naming purposeless variables, we will increase our productivity. In the context of working with JavaScript, the most common case when I run into needing an intermediate variable is when I need to operate on the array itself in the method chain.

Consider the following example:

/**
* Obtains a list of venues from a remote API, filters out venues beloning to
* a different country than `targetCountry` and describes venues in a local venue format.
*
* @param countryCode ISO 3166-1 alpha-2 country code, e.g. 'GB'.
*/
const getVenues = async (countryCode) => {
const foreignVenues = await get('http://...');

return foreignVenues
.filter((foreignVenue) => {
return foreignVenue.countryCode.toUpperCase() === countryCode.toUpperCase();
})
.map((foreignVenue) => {
return {
guide: {
fuid: foreignVenue.id
},
result: {
fuid: foreignVenue.id,
name: foreignVenue.name,
url: foreignVenue.url
}
};
});
};

Suppose the requirement has been added to ensure that all venues are unique. You could use Array#filter(), e.g.

/**
* Obtains a list of venues from a remote API, filters out venues beloning to
* a different country than `targetCountry`, ensures that all venue IDs are unique
* and describes venues in a local venue format.
*
* @param countryCode ISO 3166-1 alpha-2 country code, e.g. 'GB'.
*/
const getVenues = async (countryCode) => {
const foreignVenues = await get('http://...');

return foreignVenues
.filter((foreignVenue) => {
return foreignVenue.countryCode.toUpperCase() === countryCode.toUpperCase();
})
.filter((foreignVenue, index, self) => {
const maybeIndex = self.findIndex((maybeTargetForeignVenue) => {
return maybeTargetForeignVenue.id === foreignVenue.id;
});
      return maybeIndex === index;
})
.map((foreignVenue) => {
return {
guide: {
fuid: foreignVenue.id
},
result: {
fuid: foreignVenue.id,
name: foreignVenue.name,
url: foreignVenue.url
}
};
});
};

My first objection to this approach is that routines such as eliminating duplicate records should be abstracted using utilities. Of course, you can implement your own utility for this purpose, e.g.

const createFilterUniqueBy = (iteratee) => {
return (value, index, self) => {
return self.findIndex(iteratee) === index;
};
};

This slightly improves readability of the subject code:

/**
* Obtains a list of venues from a remote API, filters out venues beloning to
* a different country than `targetCountry`, ensures that all venue IDs are unique
* and describes venues in a local venue format.
*
* @param countryCode ISO 3166-1 alpha-2 country code, e.g. 'GB'.
*/
const getVenues = async (countryCode) => {
const foreignVenues = await get('http://...');

return foreignVenues
.filter((foreignVenue) => {
return foreignVenue.countryCode.toUpperCase() === countryCode.toUpperCase();
})
.filter(createFilterUniqueBy((maybeTargetForeignVenue) => {
return maybeTargetForeignVenue.id === foreignVenue.id;
})
.map((foreignVenue) => {
return {
guide: {
fuid: foreignVenue.id
},
result: {
fuid: foreignVenue.id,
name: foreignVenue.name,
url: foreignVenue.url
}
};
});
};

Suppose that a new requirement has been added: Now the program must check for duplicate records, and in case there are duplicate records — log the duplicate records and raise an error.

For the purpose of finding the duplicate records, you can use a utility such as find-duplicates. However, the input of find-duplicates is the subject array. Therefore, we need an intermediate array.

/**
* Obtains a list of venues from a remote API, filters out venues beloning to
* a different country than `targetCountry`, raises an error if there are
* duplicate venue IDs and describes venues in a local venue format.
*
* @param countryCode ISO 3166-1 alpha-2 country code, e.g. 'GB'.
*/
const getVenues = async (countryCode) => {
const foreignVenues = await get('http://...');

const countryVenues = foreignVenues
.filter((foreignVenue) => {
return foreignVenue.countryCode.toUpperCase() === countryCode.toUpperCase();
});
  const duplicateForeignVenues = findDuplicates(foreignVenues, (maybeTargetForeignVenue) => {
return maybeTargetForeignVenue.id === foreignVenue.id;
});
  if (duplicateForeignVenues.length) {
console.log('duplicate foreign venues', duplicateForeignVenues);

throw new Error('Found duplicate venues.');
}

return countryVenues
.map((foreignVenue) => {
return {
guide: {
fuid: foreignVenue.id
},
result: {
fuid: foreignVenue.id,
name: foreignVenue.name,
url: foreignVenue.url
}
};
});
};

There is no way around it — we need to interrupt the method chain and create an intermediate variable. This goes back to my original point: naming things is hard; we can increase our productivity if we reduce the amount of intermediate variables that we need to name.

Lets introduce a potential solution:

Array#replace()

Array.prototype.replace = function (map) {
return map(this)
};

Array#replace() method allows to operate on the array itself, i.e. we are able to obtain the entire array within a method chain and translate it or return a new value altogether, e.g.

// This is a visual explanation of the ins and outs of the function.
// Refer to the rest of the article for practical use case examples.
['B', 'D', 'F'].replace((subjectArray) => {
return [
'A',
subjectArray[0],
'C',
subjectArray[1],
'E',
subjectArray[2],
'G'
]
});
// ['A', 'B', 'C', 'E', 'F', 'G']

This becomes useful anywhere when you require to operate on the entire array within a method chain, e.g. we can rewrite our original example using Array#replace without breaking an intermediate variable/ breaking the method chain:

/**
* Obtains a list of venues from a remote API, filters out venues beloning to
* a different country than `targetCountry`, raises an error if there are
* duplicate venue IDs and describes venues in a local venue format.
*
* @param countryCode ISO 3166-1 alpha-2 country code, e.g. 'GB'.
*/
const getVenues = async (countryCode) => {
const foreignVenues = await get('http://...');

return foreignVenues
.filter((foreignVenue) => {
return foreignVenue.countryCode.toUpperCase() === countryCode.toUpperCase();
})
.replace((self) => {
const duplicateForeignVenues = findDuplicates(self, (maybeTargetForeignVenue) => {
return maybeTargetForeignVenue.id === foreignVenue.id;
});
      if (duplicateForeignVenues.length) {
console.log('duplicate foreign venues', duplicateForeignVenues);

throw new Error('Found duplicate venues.');
}
      return self;
})
.map((foreignVenue) => {
return {
guide: {
fuid: foreignVenue.id
},
result: {
fuid: foreignVenue.id,
name: foreignVenue.name,
url: foreignVenue.url
}
};
});
};

Implementing strict find method

Another example use case would be to implement a find method that throws when it cannot resolve a value, e.g.

const maybePerson = persons.find((maybeTargerPerson) => {
return maybeTargerPerson.id === 1;
});
if (!maybePerson) {
throw new Error('Person not found.');
}
const person = maybePerson;

This is a common pattern in my code, both in terms of how I use Array#find and how I name/ rename variables depending on what the variable holds.

If we hadArray#replace I could abstract this logic using a utility such as:

const createFindOne = (values) => {
return (iteratee) => {
const matchingValues = values.filter(iteratee);
if (matchingValues.length > 1) {
throw new Error('Match an unexpected number of results (more than 1).');
}
if (matchingValues.length === 0) {
throw new Error('Match an unexpected number of results (zero).');
}
return matchingValues[0];
};
};

Which would permit me to chain query and lookup methods without needing intermediate variables, e.g.

const subjectPerson = await getPersons()
.replace(createFindOne((maybeTargerPerson) => {
return maybeTargerPerson.id === 1;
}));

To find more use cases, simply pick any existing array utility and fit it into this example. Whether that is chunking, pairing or zipping — all the methods where input is the entire array will require that you break your existing method chain just to operate that method on the array.

Closing notes

I am unsure whether “replace” is the right name – the method name is open to the discussion. However, I am certain that this functionality enables abstraction that reduce code verbosity.

I ran into a need for this particular method multiple times. The last time I have started a Twitter survey:

The results weren’t favourable (26% vs 74%). Perhaps I didn’t provide enough context. The intent of this article is to build a use case and propose it to TC39.

Let me know your thoughts – How would you use Array#replace or how would you avoid using it?