3

My Problem

The ESLint airbnb policy disallows for...of loop iterations, and prefers forEach((element) => { code });. However, inner returns from that loop are swallowed - they are considered returns of the anonymous function rather than the function that embraces the loop.

Code

Original

Works, but breaks eslint-config-airbnb.

const words = ['harry', 'potter', 'and', 'the', 'forbidden', 'journey'];

const MIN_WORD_SIZE = 4;
const MAX_WORDS = 3;

function NestedIterator1() {
  const wordlist = [];
  for (const word of words) {
    if (word.length >= MIN_WORD_SIZE) {
      wordlist.push(word);
    }
    if (wordlist.length >= MAX_WORDS) {
      return wordlist;
    }
  }
  return wordlist;
}

console.log(NestedIterator1());

Alternative 1: Iterating array indices

Works, but the style is outdated and I have to manually assign values by indices.

const words = ['harry', 'potter', 'and', 'the', 'forbidden', 'journey'];

const MIN_WORD_SIZE = 4;
const MAX_WORDS = 3;


function NestedIterator2() {
  const wordlist = [];
  for (let i = 0; i < words.length; i += 1) {
    const word = words[i];
    if (word.length >= MIN_WORD_SIZE) {
      wordlist.push(word);
    }
    if (wordlist.length >= MAX_WORDS) {
      return wordlist;
    }
  }
  return wordlist;
}


console.log(NestedIterator2());

Alternative 2: Using forEach

Adheres to the style guide, but does not work - the inner returns are considered returns from the anonymous function, rather than the NestedIterator3.

const words = ['harry', 'potter', 'and', 'the', 'forbidden', 'journey'];

const MIN_WORD_SIZE = 4;
const MAX_WORDS = 3;

function NestedIterator3() {
  const wordlist = [];
  words.forEach((word) => {
    if (word.length >= MIN_WORD_SIZE) {
      wordlist.push(word);
    }
    if (wordlist.length >= MAX_WORDS) {
      return wordlist;
    }
  });
  return wordlist;
}

console.log(NestedIterator3());

My Question

How can a function iterate over an array while allowing early returns and avoiding indices and for..of iterations?

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Adam Matan
  • 128,757
  • 147
  • 397
  • 562
  • 3
    Well is it really necessary? I defer to the actual [reason in the style guide](https://github.com/airbnb/javascript#iterators-and-generators): *"Why? This enforces our immutable rule. Dealing with pure functions that return values is easier to reason about than side effects."*. Whilst I agree that avoiding mutability is grand, saying that `for..in` or `for..of` is prone to breaking that is a bit of a stretch. Therefore the rule seems questionable and probably should be overridden – Neil Lunn Nov 25 '18 at 10:34
  • throwing exception inside of `forEach` callback with `try .. catch` would work like `break` but I believe it'd be more confusing than eslint warning or per-file suppressing the rule – skyboyer Nov 25 '18 at 10:39
  • 1
    @skyboyer Agree. It's not exactly a "unique" question and has certainly been asked before [Short circuit Array.forEach like calling break](https://stackoverflow.com/q/2641347/2313887). I'd rather the OP generally acknowledge the duplicate for themselves. Certainly can't see how there would be a completely different answer from what is already there. – Neil Lunn Nov 25 '18 at 10:56

1 Answers1

3

One option would be to use reduce, which is very flexible and can be used in many situations where the other iteration methods aren't sufficient - only push to the accumulator if the accumulator's length is smaller than MAX_WORDS and of the word's length is sufficient:

const words = ['harry', 'potter', 'and', 'the', 'forbidden', 'journey'];

const MIN_WORD_SIZE = 4;
const MAX_WORDS = 3;

function NestedIterator3() {
  return words.reduce((wordlist, word) => {
    if (wordlist.length < MAX_WORDS && word.length >= MIN_WORD_SIZE) {
      wordlist.push(word);
    }
    return wordlist;
  }, []);
}

console.log(NestedIterator3());

Still, the above method does iterate over all indicies - it doesn't actually return early, it just doesn't do anything in the later iterations once the end condition has been fulfilled. If you want to actually break out of the iterator, you could use .some instead, although it's even more impure, and the intent of the code is slightly less clear IMO:

const words = ['harry', 'potter', 'and', 'the', 'forbidden', 'journey'];

const MIN_WORD_SIZE = 4;
const MAX_WORDS = 3;

function NestedIterator3() {
  const wordlist = [];
  words.some((word) => {
    if (word.length >= MIN_WORD_SIZE) {
      wordlist.push(word);
    }
    return wordlist.length === MAX_WORDS;
  }, []);
  return wordlist;
}

console.log(NestedIterator3());

For this particular example, you could also use filter followed by slice:

const words = ['harry', 'potter', 'and', 'the', 'forbidden', 'journey'];

const MIN_WORD_SIZE = 4;
const MAX_WORDS = 3;

function NestedIterator3() {
  return words
    .filter(word => word.length >= MIN_WORD_SIZE)
    .slice(0, MAX_WORDS)
}

console.log(NestedIterator3());

which certainly looks far more elegant, but the .filter necessarily iterates over all items in the array first, and so has the same problem as the reduce (no short-circuiting is going on) - in addition, those two chained methods only represent a subset of the situations in which one might want to short-circuit array iteration.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320