Back

Generating better, functional Ranges for loops

Generating better, functional Ranges for loops

Working with loops is quite common in procedural programming. But, applying some Functional Programming (FP) techniques may lead to better, clearer coding. In this article, we’re going to discuss common loops, and how they may benefit from giving an FP-look to them.

Functional-style loops

Modern JavaScript provides several ways of working with implicit loops. For instance, applying some logic to all elements of an array may be done with .map() to produce a new array as a function of the original one, or .reduce() to produce a result out of all the array elements. There are even more: some(), every(), filter(), find(), etc., all of which perform loops implicitly.

These methods are quite common in FP. Also, they are to be preferred because they allow you to program in shorter, more declarative ways. For instance, if you read someArray.map(v => aFunction(v)) you already know that the developer wants to apply a certain function to every element in some array to generate a new, transformed one. (A short aside: FP coders would probably write the sample code above as someArray.map(aFunction) — check our previous article on Pointfree Style Programming for more on this.) Of course, you still have to understand what the mapping function does. But, you won’t have to worry if the loop limits are correct, if there are “off-by-one” bug, if the function is applied correctly, etc.

When these operations aren’t appropriate, you still can resort to more general .forEach() loops. Along the same lines of the previous example, if you see code like someArray.forEach(v => doSomething(v)) you know that it will be doing something for each and every element of the array, from the first to the last. It’s possibly not so clear what it’s doing (it’s obvious that there is some kind of side effect —as we discussed in another previous entry in this series— because otherwise the code wouldn’t do anything at all - but what?) but still, using .forEach() allows us to be sure that no element in the array will be missed, or that the code won’t attempt to access elements beyond the last one, or similar common bugs.

All the loops in this section are, then, good in practice. But they aren’t the more common type of loop!

Procedural-style loops

Programming is usually taught in imperative style so it’s fairly common to see general loops written like the following.

for (let i=9; i<=22; i++) {
  // do something with i
}

Old-fashioned developers may even eschew this kind of loop, and go for a while construct!

let i = 9;
while (i <= 22) {
  // do something with i
  i++;
}

With enough experience, you can recognize these patterns and understand the implied process. In these loops we want to do something to every value in the range from 9 to 22 inclusive. However, we may make some points.

  • Off-by-one bugs are more probable. For example, you might write i<22 instead, and then the loop would end at 21, not 22.
  • If you happen to modify the index variable i, the loop may go on forever or suddenly stop.
  • Code is longer (more lines) than with the functional-style loops.

So, can we do better with some FP-style code? Let’s see how!

Functional-style ranges and looping

We have to figure out how we want to work. If we had a function that produced an array with the values to process, then we could rewrite the loop as here.

range(9, 22).forEach(i => { 
    /* do something with i */ 
})

Our range() function would have from and to parameters, and generate an array with values starting at from and ending at end. A first version could be as follows.

const range = (from, to) => {
  const arr = [];
  do {
    arr.push(from);
    from++;
  } while (to >= from);
  return arr;
};

This works well: range(9,22) produces an array [9, 10, 11, ... 22] as desired. A call with equal arguments also works: range(60,60) gets [60]. There’s a missing case: what about a reverse range, like range(12,4)? We could then also want fractional steps, so a generalization is in order.

const range = (from, to, step = Math.sign(to - from)) => {
  const arr = [];
  do {
    arr.push(from);
    from += step;
  } while ((step > 0 && to >= from) || (step < 0 && to <= from));
  return arr;
};

We’re done! This version handles all kinds of ranges, and you can even do something like range(2, 4, 0.6) and get [2, 2.6, 3.2, 3.8]. You may even skip providing the loop step, and everything will still work. However, there are some common loop features that we lack… What about breaking out, or skipping ahead? Let’s see if we can do anything about those.

Jumping around

Documentation for forEach() loops is clear: “There is no way to stop or break a forEach() loop other than by throwing an exception. If you need such behavior, the forEach() method is the wrong tool.” There’s no simple way to emulate a break or continue. Throwing an exception (and catching it out of the loop) would do for a break, but it’s a bit wordy, and not really the way to use exceptions!

There’s another “dirty trick”: we could use .some(fn) instead of .forEach(fn). As long as the fn function returns a falsy value, the loop will continue; when it returns a truthy value, the loop would end. We could thus write code as shown below — but don’t, please wait!

range(9, 22).some(i => {
  ...
  ...
  if (someCondition) return false; // <-- a "continue"
  ...
  if (somethingElse) return true;  // <-- a "break"
  ...
  ...
})

Is this good code? It works, sure, but we’re using .some() in an unintended way. Readers of the code would just become frustrated — and understanding those special return statements is not so easy. No, we need a better solution, and fortunately JavaScript has the right tool: generators.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Generating ranges

Generators are functions that can be exited but also re-entered at a later time. Each time you call a generator it will yield (return) a value, or it won’t return anything signaling that there are no more values. Code for our range() solution would be as follows. In particular, note the function* declaration, which implies a generator function.

function* range(from, to, step = Math.sign(to - from)) {
  do {
    yield from;
    from += step;
  } while ((step > 0 && to >= from) || (step < 0 && to <= from));
}

Generators cannot be used with .forEach(), but they work with for..of statements. We should now write code as follows.

for (const i of range(9, 22)) { i => { 
    /* do something with i */ 
  }
}

Quite clear! You may also directly use break and continue statements.

for (const i of range(9, 22)) { i => { 
  ...
  ...
  if (someCondition) continue;
  ...
  if (somethingElse) break;
  ...
  ...
  }
}

Oh, and if you actually wanted an array, you may spread out the generator results as follows.

const arrayFrom9To22 = [...range(9, 22)];
// this produces [9, 10, 11, ... 22]

Finally, by using generators we gain a tad of performance, because we won’t be (first) generating an array, and only then (second) processing it. You may even work with truly large ranges without requiring proportionate memory: range(9,999999999999) won’t cause any memory problems!

Summary

We have seen how to make common loops clearer by using a range-producing function, which allows us to write code more succinctly. Our first solution (producing arrays) was simple but had some limitations because it didn’t allow us to use common statements to break out of loops, for example. The final solution we considered, using generator functions, was free from those limitations and also had better performance. Get used to writing code in this fashion, and your code will be both shorter and clearer.