-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
A simpler range proposal #54
Comments
This is interesting. How do others think? |
@adit-hotstar Great write-up! I have no immediate thoughts regarding specification, implementation, functional programming, or overflow behavior. I do have an opinion on the relative simplicity for users of JavaScript. In the general case for (let number of Number.range(Math.trunc((end - start) / step)).map(i => start + step * i)) { ... } is much harder to write, read, and understand compared to for (let number of Number.rangeFromStep(start, step, end)) { ... } Using additional helper functions ( for (let number of Number.range(start, end, step)) { ... }
I suspect many people have different strongly held opinions on which solution is more elegant! My preference, regarding the |
I agree that I think that if passed only 1 argument it should be The third argument should be just a number The rest functionality could be added by iterator helpers. |
Is |
@Jack-Works yes, it's useful. However, it's the complication of the signature - object options argument. However, for me, it's not principal. |
I like the simplicity of this (without the additional helpers) - step arguments are so incredibly rare in my experience that using iterator helpers map seems quite acceptable to me. Having a different start/end, however, seems important, as this is a much more common use case. |
The use of the step argument is not rare in my experience and I would be fairly strongly opposed to leaving it out. |
The methods
A wise person once suggested this approach to get an inclusive range let inclusive = Number.range(start, end + step, step); |
Another wise person explained why it's better to have 😂😂😂😂
|
What about something like this? Number.range().map(i => start + step * i).takeWhile(n => n < end) It's a bit more readable. The only problem is that currently Number.range = function* () {
for (let i = 0; i <= Number.MAX_SAFE_INTEGER; i++) {
yield i;
}
};
BigInt.range = function* () {
for (let i = 0n; true; i++) {
yield i;
}
}; At this point, we should probably rename the function to something else like |
I have a strong preference for multiple functions instead of a single function with multiple options. The implementation of a single function with multiple options will tend to become complex. On the other hand, the implementation of each of the multiple functions can be kept simple. My preference would be to have a By the way, in terms of the sheer character count using a different function wins. Number.range(start, end, { inclusive: true }) // 45 characters
Number.inclusiveRange(start, end) // 33 characters |
My library implements a /** @private @type {!Object<string, !IteratorIterable<number>>} */
const sequences = Object.create(null);
/**
* @public
* @param {string=} name - optional
* @return {!IteratorIterable<number>} - non-null
*/
function sequence(name) {
if (name) {
if (!(name in sequences)) {
sequences[name] = range(0, Number.MAX_SAFE_INTEGER);
}
return sequences[name];
}
return range(0, Number.MAX_SAFE_INTEGER);
} which I have found useful. For example, I've used named sequences to assign consecutive IDs to named form controls. The unnamed version acts like a simplified I'd rather have a general purpose |
After reading all the comments, I've been thinking a lot about how to simplify the
These five functions can be used to generate the whole gamut of sequences.
The default value of start is 0. The default value of step is ±1 depending upon the start and end. The default value of end is unspecified. We don't use infinity or negative infinity for the default value of end because for the Sequences with a Specified EndThe Number.range = function* (start, end, step = start > end ? -1 : 1) {
if (!Number.isFinite(start)) {
throw new TypeError(`Expected start to be a finite number but got ${start}`);
}
if (!Number.isFinite(end)) {
throw new TypeError(`Expected end to be a finite number but got ${end}`);
}
if (!Number.isFinite(step)) {
throw new TypeError(`Expected step to be a finite number but got ${step}`);
}
if (step === 0) {
throw new RangeError(`Expected step to be a non-zero number but got ${step}`);
}
const length = Math.trunc((end - start) / step);
for (let i = 0; i < length; i++) {
yield start + step * i;
}
}; Sequences with an Unspecified EndThe Number.step = function* (step, start = 0) {
if (!Number.isFinite(step)) {
throw new TypeError(`Expected step to be a finite number but got ${step}`);
}
if (step === 0) {
throw new RangeError(`Expected step to be a non-zero number but got ${step}`);
}
if (!Number.isFinite(start)) {
throw new TypeError(`Expected start to be a finite number but got ${start}`);
}
for (let i = 0; i <= Number.MAX_SAFE_INTEGER; i++) {
yield start + step * i;
}
}; |
I think splitting |
I'm fine with not having I'd encourage you to copy my implementation of > let it = Number.step(1);
> it.next();
{ value: 0, done: false }
> it.next();
{ value: 1, done: false }
> it.next();
{ value: 2, done: false }
> it.next();
{ value: 3, done: false }
> it = Number.step(-1, 10);
> it.next();
{ value: 10, done: false }
> it.next();
{ value: 9, done: false }
> it.next();
{ value: 8, done: false }
> it.next();
{ value: 7, done: false } |
The behavior of |
I stand corrected. Using
I'll admit that the end is not const it = Number.range(0, Infinity, 1e307);
console.log(it.toArray().length); // 18 I would have expected Now, the question is whether this is the desired behavior? |
From the user point of view (and my personal one as well), I agree with @Andrew-Cottrell, it's way easier to have a simple function to do a range passing a start, end, and an optional step parameter would cover 99% of the cases. I think the idea of having other functions that slightly change the behavior of the original one is a bit to add more things to learn for a language with already a lot of things to learn... And the fact that you needed a comparison table to show the differences between Besides, I think it makes more sense to think of a The optimal idea for me would be something as suggested before with: for (let number of Number.range(start, end, step)) { ... } Or even better, as other languages do:
|
There seems to be a lot of If Infinity is involved, then there needs to be a lazy way to access... |
How about dynamic generation of values in, say, an array? |
@ogbotemi-2000 that's already what the proposal would do - |
I agree, do tell me however which of the lines of codes below is better
Personally, I'd choose the latter anyday hence why I suggest that redundant methods such as Any thumbs ups? |
@ogbotemi-2000 they both work, you can choose either way you like. |
I'll close this issue for now for housekeeping. At today's TC39 meeting, delegates are generally satisfied with the status quo API. |
Hold on a second, This will reduce the rather needless code present in the current definition of |
@ogbotemi-2000 yes, but this proposal does it lazily. |
I am taking lazily to mean on a need to use basis. If not then, do explain. |
yes, that's right. it generates one number at a time instead of all the numbers at once. |
From the tone of your voice, I could tell that you're excited by this lazy perk baked into the current proposal, that's good. Object.create([].values(), {
{
next:{
value:_=>{/*return {value:<Number>, done:<Boolean>} based on internal conditions */ }
}}
})
The lazy bit comes to light when const generatorPrototype = Object.getPrototypeOf(Object.getPrototypeOf((function* () {})()));
const origNext = generatorPrototype.next
/*next then gets a new definition pertaining to Iterator.range via new Proxy(next ...)*/ IMHO the approach used above to get a reference to the internally used You may agree that the first code block makes it easier to define what The second code block, aside the risk of prototype pollution, is just not right being that a reference to Further more, having to decide which reference of (function*(){})().__proto__.next
/*same next on both prototypes?*/
(function*(){})().__proto__.__proto__.next I therefore advise the delegates in charge to test and consider the first code block in this reply as an alternative. |
Yes. It is an iterator. You can use it with for of loop. for (const i of Iterator.range(0, 10)) |
@ogbotemi-2000 Looks like you're referencing the polyfill behavior, please don't. You should check out the specification: https://tc39.es/proposal-iterator.range/ |
I do not understand what you mean, please explain @Jack-Works |
It means that you've referred to "the code", but a language proposal only has a spec. a proposal repo's polyfill is just for illustrative purposes, and isn't how it will actually be implemented. |
Thank goodness. |
@ljharb can you please go through the message I sent regarding the alternative to the current proposal? I'd like to hear your thoughts on how practical a code it is and whether it can be adopted for the actual implementation of |
Your suggested alternative is very unclear - the proposal already returns an iterator, that can be used with anything that works with iterators. |
Okay then, as long as the current proposal isn't what will be the body of |
Currently, the range proposal is very complicated. Thanks to iterator helpers we can greatly simplify the
range
function.The simplified
range
function always returns the sequence of integers starting from 0. The programmer can then use the.map
iterator helper to convert the sequence of integers into the desired sequence.We could also provide additional helper functions for generating specific sequences.
This simplifies our table considerably.
Advantages
range
function is greatly simplified.range
function promotes the use of iterator helpers and functional programming.Number.range(to)
vsNumber.range(from)
debate elegantly.Number.range(n)
generates a finite sequence of integers from 0 to n-1, like in Python.Number.range()
generates an infinite sequence of integers, like in Haskell.n
can't be bigger thanNumber.MAX_SAFE_INTEGER
. Currently, the overflow behavior is not yet decided. Note that even if we generate a number every microsecond, it will still take almost 286 years to generate all the numbers from 0 to MAX_SAFE_INTEGER.The text was updated successfully, but these errors were encountered: