Thursday, December 12

These 2 Unpopular Functions Will Improve How You Manipulate JavaScript Array Data

JavaScript has come a long way, it’s progressively evolving at a compelling rate and winning an army of followers by the day. The modern JavaScript developer is arguably grappling with increased complexity than a few years ago, but is more armed with better native language features, a solid tooling ecosystem and a vibrant community.

Introduction

ES5 and ES6 brought a ton of improvements to JavaScript, including better ways of working with a collection of data expressed as arrays. While the language has significantly improved support for declarative data manipulation on arrays, and modern browser support for them continue to be great, JavaScript developers are only adopting a limited subset of array capabilities – generally excluding those that should further improve how they manipulate arrays, and potentially give them performance gains.

 

The Prominent JavaScript Array Functions

Usage of .map(), .filter(), and .reduce() are very common. For the purpose of this article (and posterity if you don’t mind), lets call these three functons MFR.

They have gained more prominence over the other functions for manipulating array data in javascript. The very curious side of me would love to dig into this phenomenom to demonstrate beyond any doubt that this is the case, and try to see what reasons or predisposing factors I can establish.

For instance, I would have loved to examine a number of open source repositories, run the codebases through some static analysis tool (not too hard to build with ASTs) and compare the adoption of various array manipulation functions across the codebases. If I ever make out the time to do this, I will definitely publish my findings.

In the meantime, I did two simple things to help validate my hypothesis, and so far, they seem accurate.

Google Trends show more interest in MFR

Google Search also show that search queries for MFR produce far more results.

Search results for .filter() are as much as over 74M! This is 99.97% higher than results for .every(), and 99.98% higher than results for .some()!

This means people are talking, writing and teaching more MFR, creating more interests which is resulting in more searches, and ultimately more usage over time.

Are these the only or best ways to manipulate data in Javascript arrays these days?

Enter .every() JavaScript Array Conversation with .some() More Thought

JavaScript developers have probably been implicitly conditioned to limit computations on arrays to iteration (.forEach) and transformation (.map, .filter, .reduce – a.k.a MFR) operations. Hence not letting them see where more .some() and .every() should easily come in.

The biggest problem here (and likely an anti-pattern) is how I believe developers unknowingly coerce computations that should naturally be done with .some() or .every() into MFR , especially a sequence of .filter() followed by a .reduce().

Almost every usage of MFR where you should be exiting early and not combing through the entire collection, is a prime candidate for .some() or .every() – because they both short-circuit and exit the array iteration as early as possible, instead of aimlesly running till the end and potentially wasting compute resources.

In a time when we are building more complex apps and shipping more bytes to the client, resulting in significant overhead to parse Javascript (see TTI), anything we can do to have 3 iterations (due to short-circuit) instead of 30 – especially on mobile, is highly welcome.

Array.prototype.every()

The every() method allows us establish if every element within an array meets a certain reqirement. It stops further evaluating the array (short circuit) the first time it finds an element that does not satisfy the given requirement.

The every method executes the provided callback function once for each element present in the array until it finds one where callback returns a falsy value. If such an element is found, the every method immediately returns false. Otherwise, if callback returns a truthy value for all elements, every returns true. callback is invoked only for indexes of the array which have assigned values; it is not invoked for indexes which have been deleted or which have never been assigned values.This method returns true for any condition put on an empty array. -MDN

Array.prototype.some()

The some() method allows us establish if some (at least one) elements within an array meets a certain requirement. It stops further evaluating the array (short circuit) the first time it finds an alement that does satisfy the given requirement.

some() executes the callback function once for each element present in the array until it finds one where callback returns a truthy value (a value that becomes true when converted to a Boolean). If such an element is found, some() immediately returns true. Otherwise, some() returns false. callback is invoked only for indexes of the array which have assigned values; it is not invoked for indexes which have been deleted or which have never been assigned values.This method returns false for any condition put on an empty array. – MDN

One Example, Two Scenarios

You have been asked to write a simple add() function to add a bunch of integers. The function should expect to be given any number of integers to be added and it should return their sum after computing the addition

Scenario One

If the add() function is given mixed input (e.g integers, floats, undefined, strings, e.t.c), it should proceed with just the integers and return the sum from adding them.
We could implement our add() function this way:

const add = (...entries) => {
  return entries
    .filter(Number.isInteger)
    .reduce((sum, int) => {
      return sum + int;
    }, 0);
};

Since this scenario expects the add() function to compute the sum from whatever integers exists within the given inputs, one might implement the function like it is above. We first use .filter() to extract only the integers and then compute the sum with a .reduce(). The .filter() operation will however iterate over the entire array even if there are no integers within it. Quite wasteful.

One can also argue that we can get a better solution like this below

const add = (...entries) => {
  return entries.reduce((sum, entry) => {
    if(Number.isInteger(entry)) {
      return sum + entry;
    }
    return sum;
  }, 0);
};

This take on the add() is a little better because, unlike the first one that iterates to identify valid data and then further iterates to compute the result based on the valid data, now there is just one block of iteration, although we are still running through to the end of the array even if there might be no integers within it.

Enter .some()

We need a way to first tell if the gathered inputs have at least one integer. Truth is, since we are trying to find the sum from the inputs, we actually need at least two integers in there. So we will proceed to reduce the inputs to the sum of her integers if we can find two or more integers. Lets use .some() so ensure this contidion is met so we dont aimlessly run through a potentially large array

const add = (...entries) => {
  let theSum = 0;
  if(hasTwoOrMoreInts(entries)){
    // there are >= 2 integers, lets sum them
    theSum = entries.reduce((sum, entry) => {
      if(Number.isInteger(entry)) {
        return sum + entry;
      }
      return sum;
    }, 0);
  }
  return theSum;
};

So now, we have a condition that prevents the sum calculation unless we are sure there are two or more intergers. We are doing this with a hasTwoOrMoreInts() function which we will be creating below

const hasTwoOrMoreInts = (entries) => {
  let lastIndex = -1;
  let hasMinimumIntsCount = false;

  const hasAnInt = entries.some((entry, index) => {
    lastIndex = index;
    return Number.isInteger(entry);
  });

  if(hasAnInt === true) {
    // we've got one int, is there another?
    const hasMoreInts = entries.slice(lastIndex + 1).some(Number.isInteger);
    hasMinimumIntsCount = (hasMoreInts === true) && hasAnInt;
  }

  return hasMinimumIntsCount;
};

Scenario Two

If the add() function can receive mixed input (e.g integers, floats, undefined, strings, e.t.c) but needs to only proceed with computing the sum of the given inputs if all the inputs are integers, a common approach influenced by the prominence of MFR might look like this:

const add = (...entries) => {
  let theSum = 0;
  const nonInts = entries.filter(entry => !Number.isInteger(entry));
  if(nonInts.length === 0) {  // are there non-ints?
    theSum = entries.reduce((sum, int) => {
      return sum + int;
    }, 0);
  }
  return theSum;
}

Again entries.filter(...) attempts to see if there are invalid inputs by iterating the entire entries array to gather every input that is not an integer. If there are no invalid inputs (nonInts.length === 0), we compute the sum with .reduce(...). This would have made sense if we needed the total number of all invalid inputs or all the inputs themselves. Otherwise, we should be looking for the very first invalid input and deciding what next from there.

Enter .every()

Like .some(...), .every(..) will act on the least information necessary. In this case, once it finds an input that is not an integer, it will look no further (by exiting with a return of false) and allow us proceed with the next important thing. Let’s see how .every() does this for us

const add = (...entries) => {
  let theSum = 0;
  const areAllInts = entries.every(Number.isInteger);
  if(areAllInts === true) {  // are these indeed all ints?
    theSum = entries.reduce((sum, int) => {
      return sum + int;
    }, 0);
  }
  return theSum;
};

Since entries.every(...) will return false as soon as it finds what is not an integer, we are able to exit further tests for invalid elements within entries, freeing up resources that might be needed to give a mobile user a smooth scrolling experience!

A More Practical Example

I know you dont write add() functions that often in your coding life. So I thought to describe something you probably do a lot. Lets apply .every() and .some() to a hypothetical HTML form validation, shall we!

We should attempt to submit the form only after all required data have been obtained and validated from the form. We equally want at least one of the optional data to be filled in.

const requiredFields = Array.of(
  isFirstNameValid(),
  isLastNameValid(),
  isEmailValid(),
  isAboutMeValid()
);

const optionalFields = Array.of(
  isTwitterValueValid(),
  isFacebookValue(),
  isGoogleplusValueValue()
);

const isValid = (inputStatus) => inputStatus === true;

if(requiredFields.every(isValid) && optionalFields.some(isValid)) {
  // all required fields are valid
  // and at least one social media field is valid
  // lets proceed to submit the form now :D
} else {
    // lets tell the user we are serious here!
    // this will happen really fast since we are short-circuting 
    // with .some and .every above
}

As seen above, all the functions within Array.of() are doing the validation for specific form fields, and then return either true or false. Since Array.of() is a factory for constructing arrays from the parameters it is given (the validation functions in our case), this means optionalFields can eventually look like [true, false, false]. This way, if any value in requiredFields is false, or if none of optionalFields is true, we do not submit the form. The form submission is decided with the shortest path of execution.

Conclusion & Going Forward

This exploration helped reveal how prominent MFR have become. It also sheds light on why you should be more open minded when manipulating your arrays. This lets you see where other array methods outside MFR should be used instead.

Below are possible scenarios to promote or adopt .some() or .every() over MFR in your Javascript codebase

  1. A .filter() immediately followed by .forEach(), or .map(), or .reduce() or any chained combination involving them
  2. A .filter() immediately followed by a condition examining the result of the invocation, and where the body of the condition contains a .forEach() , or .map(), or .reduce() or any chained combination involving them, acting on the result from the .filter()

For these and other relevant scenarios, you want to ask yourself if you should be exiting the checks (validation usually done with .filter()) as soon as possible, and see if you are better off with .some() or .every()

Source: Scotch.io

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x