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 loops that initialize arrays. Please explain how I have succeeded or failed to do so, including any use cases this doesn't cover.
Code for Review
const sign = Math.sign;
/**
* Returns true if arguments are in ascending or descending order, or if all
* three arguments are equal. Importantly returns false if only two arguments
* are equal.
*/
function ord(a, b, c) {
return sign(a - b) == sign(b - c);
}
/**
* Three overloads:
*
* range(start, end, step)
* yields start, start + step, start + step*2, ..., start + step*n
* where start + step*(n + 1) would equal or pass end.
* if the sign of step would cause the output to be infinite, e.g.
* range(0, 2, -1), range(1, 2, 0), nothing is produced.
*
* range(start, end)
* as above, with step implicitly +1 if end > start or -1 if start > end
*
* range(end)
* as above, with start implicitly 0
*
* In all cases, end is excluded from the output.
* In all other cases, start is included as the first element
* For details of how generators and iterators work, see the ES6 standard
*/
function* range(start, end, step) {
if (end === undefined) {
[start, end, step] = [0, start, sign(start)];
} else if (step === undefined) {
step = sign(end - start) || 1;
}
if (!ord(start, start + step, end)) return;
var i = start;
do {
yield i;
i += step;
} while (ord(start, i, end));
}
/*
Use Cases. Feedback on output method is not necessary. Insights on use
cases themselves are welcome.
*/
(function shortFor(ol) {
for (var i of range(0, 16, 2)) ol.append($("<li>").text(i.toString()));
})($("#short-for .vals"));
(function spreadMap(ol) {
for (var el of [...range(4)].map(x => x * x)) {
ol.append($("<li>").text(el.toString()));
}
})($("#spread-map .vals"));
(function correspondingIndex(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()));
})($("#corresponding-index .vals"));
<!--
jQuery is included here to conveniently emit results of test cases;
the code to be reviewed has no dependencies.
Feedback on HTML is not necessary.
-->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>
<div class="use-case" id="short-for">
<h1>Use Case: Less verbose simple for loop</h1>
<code>for (var i of range(0, 16, 2))</code>
<p>Values of i:
<ol class="vals"></ol>
</div>
<div class="use-case" id="spread-map">
<h1>Use Case: Initialize an array without push()</h1>
<code>[...range(4)].map(x => x*x)</code>
<p>Array contents:
<ol class="vals"></ol>
</div>
<div class="use-case" id="corresponding-index">
<h1>Use Case: Add corresponding elements of two arrays</h1>
<code>
var a = [1, 2, 3], b = [4, 5, 6];
for (var i of range(a.length)) a[i] += b[i];
</code>
<p>Contents of array a:
<ol class="vals"></ol>
</div>
</body>