ECMAScript proposal, specs, tests, and reference implementation for Numbers as iterators.
This initial proposal was drafted by @johnhenry with input from @_ and @_.
Designated TC39 reviewers: @_ @_
- Table of Contents
- Status
- Rationale and Motivation
- Prior Art
- Ruby
- Python
- Coffeescript
- Proposed Solution: Make Numbers Iterable
- for-of loop
- conversion to array
- conversion to set
- set-theory
- variadic functions
- infinite loops
- destructuring
- negative numbers
- combinations
- Specification
- Implementation
- Paste
- Import
- Alternate Solution: Add Static Method to Number Object
- FAQ
- How does one interate between arbitrary integers?
- Does this cause language conflicts?
This proposal is currently at stage 0 of the process.
There are a few ways to loop through a ordered list of integers, 0 through n-1 in JavaScript.
Using a for loop:
//for syntax
for(let i=0; i < n; i++){
//do something with i
}
Using a while loop:
//while syntax
let i = 0;
while(i < n){
//do something with i
i++;
}
Both syntaxes are awkward syntax for a number of reasons:
-
they require an explicit, non-constant, declaration of a placeholder,
let i = 0
-
they requires an explicit test for said placeholder
i < n
-
they require explicit incrementation step of said placeholder
i++
In Ruby, this can be achieved more elegantly using a number's times method:
#number#times syntax
n.times do |i|
#do something with i
end
This can also be achieved with Ranges.
#range#each syntax
(0..n).each do |i|
#do something with i
end
#for-in syntax
for i in 0..n
#do something with i
end
Python also achieves this with the concept of Ranges.
#for-in syntax
for i in range(n):
#do something with i
Coffeescript also achieves this with the concept of Comprehensions over Ranges.
#for-in syntax
(#do something with i
for i in [0..n-1])
We can do away with most of the awkwardness using the iterator protocol to make numbers iterable.
Given a positive number n, invoking n[Symbol.iterator]
will return an iterator yielding the sequence [0, n) or 0 up to n - 1, inclusive. When n is negative, the iterator instead yields the sequence [n, 0) or n up to -1, inclusive. This definition for negative numbers is useful as it allows a method of iterating between two arbitrary integers. (See the FAQ for more details.)
An iterable number can be the target of a for-of loop, mirroring the for-in syntax from other languages:
//for-of syntax
for(const i of n){
//do something with i
}
Iterable Numbers can be turned into arrays and use their methods, mimicking Ruby's range#each syntax and allowing other operations.
[...n].forEach(i=>{
//do something with i
});
[...n].map(i=>{
//do something with i
});
[...n].filter(i=>{
//do something with i
});
[...n].reduce((_,i)=>{
//do something with i
});
Array.from works similarly for Iterable Numbers.
Array.from(n);//[0, 1, ... n-1]
Like any iterator, iterable Numbers can be converted into sets.
new Set(n);//Set {0,1,...n-1}
On a side note, this fits well with the set theory of integers.
Compare the definition of von Neumann ordinals
n := {x in N | x < n}
0 := {}
1 := {0}
2 := {0, 1}
3 := {0, 1, 2}
...
to results here.
const zero = new Set(0);//Set {}
const one = new Set(1);//Set {0}
const two = new Set(2);//Set {0, 1}
const three = new Set(3);//Set {0, 1, 2}
//...
Iterable Numbers can be spread across variadic functions.
const sum = (...numbers)=>numbers.reduce((a, b)=>a+b, 0);
sum(...n);//0 + 1 + ... n-2 + n-1 === (n(n-1))/2
Using Infinity
as an iterator provides a convenient method for potentially infinite loops.
Compare what we might have done before:
let i = 0;
while(true){
//do something with i
if(/*some condition*/){
break;
}
i++;
}
to what we would do now
for(const i of Infinity){
//do something with i
if(/*some condition*/){
break;
}
}
Iterable numbers can be destructured as arrays.
const [,one,,three] = Infinity;//
one;//1
three;//3
Iterable negative numbers yield the sequence from -n up to -1 instead of 0 up to n-1.
[... -n];//[-n, -n+1... -1]
Iterable numbers can be combined with other iterable objects.
[...-n, ...[0,0], ...n+1];//[-n, -n+1... -1, 0, 0, 0, 1 ... n-1, n]
When Number.prototype[Symbol.iterator] is called, the following steps are taken:
- let N be Math.floor(this value)
- if N is 0, return a generator in a "done" state.
- if N is Positive, return a generator that yields numbers 0 to N-1
- if N is Negative, return a generator that yields numbers N to -1
The following code modifies the Number object's prototype, allowing any positive number, n, to be used as an iterator, yielding 0 up to n-1. (technically, the floor of n-1). Negative numbers return a generator yielding n up to -1.
You can paste the following code into a console:
Object.defineProperty(
Number.prototype,
Symbol.iterator,
{ value: function *(){
const max = Math.max(0, Math.floor(this));
let i = Math.min(0, this);
while(i < max) {
yield i;
i+=1;
}
return this;
}
});
and then try out the above examples to see this proposal in action.
You can import the included "make-numbers-iterable.js" into a project.
import './make-numbers-iterable';
//...
and then try out the above examples to see this proposal in action.
Alternatively, we might add an "interator" method to the Number object.
for (const n of Number.iterator(number)){
//do something with n
}
This can be implemented with the following code.
Object.defineProperty(
Number,
"iterator",
{ value: function *(num){
const max = Math.max(0, Math.floor(num));
let i = Math.min(0, num);
while(i < max) {
yield i;
i+=1;
}
return num;
}
});
Iterate up to the difference and add the smaller to each the iteration
for(const i of (larger - smaller)){
//do something with (smaller + i)
}
This will produce the sequence:
[smaller, smaller + 1, ..., larger - 1]
Because of our definition for negative integers, it doesn't matter which numbers is smaller, as long as the subtrahend is added to the iteration. The following code produces the same sequence as the code above:
for(const i of (smaller - larger)){
//do something with (larger + i )
}
Currently if a number is given where an iterator is expected, a type error is thrown:
TypeError: (var)[Symbol.iterator] is not a function(…)
As such, this would not cause conflicts with existing working code.