HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

Range iterator in ES6 similar to Python and Ruby for

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
iteratorrangees6pythonforrubyandsimilar

Problem

Background and Purpose

For those unaccustomed to Ruby, Ruby supports range literals i.e. 1..4 meaning 1, 2, 3, 4 and 1...4 meaning 1, 2, 3. This makes a Ruby for loop pretty sweet:

for i in 1..3
    doSomething(i)
end


It works for non-numeric types and that's pretty cool but out of our scope.

A similarly convenient thing exists in Python, range, with some additional features and drawbacks. range(1, 4) evaluates to [1, 2, 3]. You can also provide a step parameter via range(0, 8, 2) which evaluates to [0, 2, 4, 6]. There is no option to make the last element inclusive (as far as I know). range calculates its elements the moment it is invoked, but there is also a generator version to avoid unnecessary object creation. In combination with Python's list construction style, you can do all sorts of cool stuff like [x*x for x in range(0, 8, 2)], which is right on the border of this question's scope.

Now that ES6 has generators and the for-of statement (and most platforms support them), iteration in JavaScript is quite elegant; there is generally only one truly right (or at least best) way to write the loop for your task. However, if you are iterating over a series of numbers, ES6 offers nothing new. That's not just in comparison to ES5, but to pretty much every curly-bracket-based language before it. Python and Ruby (probably other languages I don't know, too) have proven that we can do it better and eliminate stroke-inducing code like:

while (i--) {
    sillyMistake(array[i--]);
}

for (
    var sameThing = youHaveWrittenOut;
    verbatimFor >= decades;
    thinkingAboutDetails = !anymore
) {
    neverInspectAboveForErrors();
    assumeLoopVariantIsNotModifiedInHere();
    if (modifyLoopVariant()) {
        quietlyScrewUp() || ensureLoopCondition() && causeInfiniteLoop();
    }
}


ES6 for-of, spread operator, and this generator can eliminate all manual loop counter fiddling, unreadable three-part for, and smelly loop

Solution

However, if you are iterating over a series of numbers, ES6 offers nothing new.

True, they never put in a range function. But it's pretty easy to build one yourself using the newer APIs. I'm talking about fill and map. My logic may be off here (not really sure about range's intricacies) but the following should give you a general idea of how it looks like.



function range(start, end, step) {
var _end = end || start;
var _start = end ? start : 0;
var _step = step || 1;
return Array((_end - _start) / _step).fill(0).map((v, i) => _start + (i * _step));
}

document.write(JSON.stringify(range(10)));
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

document.write(JSON.stringify(range(1, 11)));
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

document.write(JSON.stringify(range(0, 30, 5)));
// [0, 5, 10, 15, 20, 25]




Above, we just create an empty array with the length needed then we use map to fill in the values. What this returns is a regular array of numbers, nothing fancy. However, it makes it possible to use array methods like forEach, map etc which are more or less equivalent to what you are looking for.

Array.apply(null, Array(5));


fill is relatively new. The older way to do this was mentioned in this answer. It used the Array.apply to force the array to not have empty slots so array methods like map can traverse through it.

Now, to your test cases. When we use the approach mentioned above, it's relatively easy to chain array methods one after the other.

for (var i of range(0, 16, 2))  ol.append($("").text(i.toString()));

// becomes

range(0, 16, 2).forEach(i => $("").text('' + i).appendTo(ol));

////////////////////////////////////////////////////////////////////////////

for (var el of [...range(4)].map(x => x * x)) {
  ol.append($("").text(el.toString()));
}

// becomes

range(4).map(x => x * x)
        .forEach(i => $('li').text('' + i).appendTo(ol));

////////////////////////////////////////////////////////////////////////////

var a = [1, 2, 3],
    b = [4, 5, 6];
for (var i of range(a.length)) a[i] += b[i];
for (var el of a) ol.append($("").text(el.toString()));

// becomes

range(a.length).map(i => a[i] + b[i])
               .forEach(i => $('li').text('' + i).appendTo(ol));


As for the other code...

for (var i of range(a.length)) a[i] += b[i];


It would be best if you returned a new array containing the results of the added a and b. It's not wrong (hey, it's programming, anything goes), but it will save you from future headaches by not worrying about who mutated a. For instance, you need to do another operation with a somewhere else but oops! Some operation already mangled the values!

ol.append($("").text(i.toString()));

// to

$("").text('' + i).appendTo(ol);


Instead of using containerReference.append(constructedElement) in jQuery, reverse the order into constructedElement.appendTo(containerReference). It will look more streamlined, avoiding the cluttery parens.

Also, a quick way to turn anything into a string is to prepend a blank string. Since we started a string, JS will start to coerce the succeeding values to a string.

eliminate all manual loop counter fiddling, unreadable three-part for, and smelly loops that initialize arrays.

Consider using array methods like forEach, map etc. when running through arrays instead of loops. Most of them provide 3 arguments: current value, current index, and a reference to the original array being traversed. The index is handy for counting and you avoid managing it yourself. The reference to the array traversed is handy especially when you chain together operations and don't want intermediate references to the arrays generated.

range(4).map(x => x * x).forEach((v, i, a) => {
  // a = [0, 1, 4, 9], the original array generated by map
});

Code Snippets

Array.apply(null, Array(5));
for (var i of range(0, 16, 2))  ol.append($("<li>").text(i.toString()));

// becomes

range(0, 16, 2).forEach(i => $("<li>").text('' + i).appendTo(ol));

////////////////////////////////////////////////////////////////////////////

for (var el of [...range(4)].map(x => x * x)) {
  ol.append($("<li>").text(el.toString()));
}

// becomes

range(4).map(x => x * x)
        .forEach(i => $('li').text('' + i).appendTo(ol));

////////////////////////////////////////////////////////////////////////////

var a = [1, 2, 3],
    b = [4, 5, 6];
for (var i of range(a.length)) a[i] += b[i];
for (var el of a) ol.append($("<li>").text(el.toString()));

// becomes

range(a.length).map(i => a[i] + b[i])
               .forEach(i => $('li').text('' + i).appendTo(ol));
for (var i of range(a.length)) a[i] += b[i];
ol.append($("<li>").text(i.toString()));

// to

$("<li>").text('' + i).appendTo(ol);
range(4).map(x => x * x).forEach((v, i, a) => {
  // a = [0, 1, 4, 9], the original array generated by map
});

Context

StackExchange Code Review Q#115704, answer score: 3

Revisions (0)

No revisions yet.