tc39 / proposal-iterator.range

A proposal for ECMAScript to add a built-in Iterator.range()

Home Page:https://tc39.es/proposal-iterator.range/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Make Number.range() return re-usable value

sffc opened this issue · comments

I would consider separating the iterator from the object returned from Number.range(), such that Number.range() returns an immutable object (with getter properties like .from, .to, etc), and then calling [Symbol.iterator] returns a "fresh" iterator with a .next() method. You can re-use the ranges. In other words, we should consider making the following code work:

const range = Number.range(0, 5);
for (let i of range) {
  // 0, 1, 2, 3, 4
}
for (let i of range) {
  // 0, 1, 2, 3, 4
}

This is what we are doing in the Intl.Segmenter proposal.

https://github.com/tc39/proposal-intl-segmenter/issues

CC @gibson042

This is sort of what I assumed the interface would be as well: Number.range() would return an iterable, so for-of loops work over it just fine. However, for the example in the README using iterator helpers, it'd be a little more verbose: you'd need Iterator.from(Number.range(...)).take(...). I could see the tradeoff either way, but mentally, for me, the "default" design would be an iterable.

So reusable and easy to use iterator helpers are conflict...
I prefer the latter one but the reusable is also important.
Maybe add a .clone() to return a new clear RangeIterator with the same from, to and step?

We decided against returning an iterator from Intl.Segmenter.prototype.segment() after long discussions about ergonomics (see, e.g., tc39/proposal-intl-segmenter#93, and other issues in that proposal). Basically, if the return value of Number.range(...) should be considered a "thing", then you should make it have its own prototype that's an iterable but not an iterator.

I think we should explore other options here. For example, if you write Number.range(...)[Symbol.iterator].take(...), does that feel more ergonomic than Iterator.from(Number.range(...)).take(...)?

Iterator.from(...) and [Symbol.iterator] is basically the same in this case, they are both not ergonomics for chaining iterator helpers on it.

So reusable and easy to use iterator helpers are conflict...

Yes, this is a thing that's been under discussion for iterator helpers in general. It's also important to be able to use them for Arrays, which are iterable but not iterators.

I liked this story: You can use Iterator.from for these situations, and we expect this to be ergonomic enough. (If we don't, I wonder if we could make some change to the iterator helpers proposal for a more ergonomic path...)

commented

Iterators are already iterable, so the only discussion point seems to be re-usability. I'd argue if you want to reuse it you should call it again, it's more explicit and its weird to me that you'd need to make two calls (range()[Symbol.iterator]()) to begin consuming an iterator you directly asked for.

My intuition around the iterator protocol was that we're sort of generalizing part of the functionality Arrays. Arrays support [Symbol.iterator] to iterate through them, and other objects do too. I was picturing that Number.range would produce something which was also this same kind of "generalization of an Array", and that's why my intuition was that it wouldn't be an iterator, but rather a reusable iterable.

@Jack-Works Patching it up with .clone() would restore the functionality kinda, but it wouldn't meet the goal of making Number.range follow a common protocol to other iterable objects. Maybe we could make .clone() a more general thing, but I'd like to see if we could continue following the split of the initial iterable/iterator protocol, which already handles these two things.

@devsnek What in particular is weird about this? If you have a range function that returns an Array, you'll also need to call the Symbol.iterator method to iterate over it. Accordingly, since they should also work for Arrays, lots of usage sites will be calling Symbol.iterator anyway.

It's the way generators already work - the generator is called (perhaps with arguments) to produce the (iterable) iterator. The generator itself is not iterable, you have to call it every time you want to iterate.

@ljharb I agree with this description of what generators are, but I don't see how that applies here and why Number.range shouldn't return something analogous to Arrays.

commented

The correct pattern here is definitely to return an iterator. If we want iterables for some reason the pattern you're looking for is a Number.Range class.

If it helps:
Class -> Instance (Iterable or callable) -> Iterator
Array -> array -> ArrayIterator
Map -> map -> MapIterator
Set -> set -> SetIterator
String -> string -> StringIterator
Number.Range -> numberRange -> NumberRangeIterator
Generator -> generator -> GeneratorIterator (Number.range ~= generator)
Array -> array.entries -> ArrayIterator
Array -> array.values -> ArrayIterator
Map -> map.entries -> MapIterator
Map -> map.values -> MapIterator
Map -> map.keys -> MapIterator
...

@devsnek OK, interesting analysis. I'll have to think about this some more. I'm wondering how/whether it'd apply to @sffc 's example of Intl.Segmenter (which is definitely an instance method, but also a slightly awkward case due to the random access methods).

commented

Another way to think of it: JS moved reusability out of the "Iterator" protocol itself (f() -> iterator is very convenient). Iterable is just the reification of "object with Symbol.iterator".

@littledan I'm admittedly not very familiar with the Intl.Segmenter proposal but a cursory glance seems to be this, which holds to the pattern but is kind of weird:
Intl.Segmenter -> segmenter -> segmenter.SegmentsForString (via segmenter.segment() helper) -> segmentsForString -> SegmentsForStringIterator

It's worth noting that Intl has always had different patterns compared to the rest of the stdlib, most notably in their constructor patterns (Intl.NumberFormat(x).format(n) vs hypothetical Intl.formatNumber(x, n))

Well, I think Intl constructors are a good design that could make sense among anything TC39 creates, but we're getting a bit off-topic. For new APIs, I'd like to see if we can use common design patterns that make sense in general, and having Intl.Segmenter follow the iteration protocol (unlike Intl.v8BreakIterator) was intended to be part of that.

commented

my point was basically that both Number.range() and Intl.Segmenter().segement follow the pattern as far as i can tell, but i wouldn't really equate one directly to the other, because they have to (and do) represent different things.

On topic again, Number.range is itself the reusable bit (that is the generator pattern). You can just do const whateverRange = () => Number.range(from, to); and then you get the explicit bonus of having to do for (const item of whateverRange()) instead of for (const item of whateverRange) where its not immediately apparent whether or not you're doing something reusable (because as i said above, Iterable in js only means there is a Symbol.iterator method, not that it will create fresh state)

Another thought: (working but it's a crazy design)

Add a @@iterator on the iterator itself to make it auto-cloneable.

const range = Number.range(0, 5);
for (let i of range) { } // use range[@@iterator] which return a cloned range
for (let i of range) { }  // use range[@@iterator] which return a cloned range
// range is not consumed yet.
range.take(...).toArray() // now it's consumed
commented

iterators already have @@iterator on them. But aside from that, doesn't

for (let i of range()) { }
for (let i of range()) { }

seem like a much more obvious way of saying you're using a separate range for each one? I don't understand why you want to make that implicit.

An Intl.Segmenter Segments instance, like an array/set/map/string/etc., is Iterable but not itself an Iterator—it is primarily a factory for constructing iterators, and also supports a containing method of its own for returning a single random-access iteration result without actually constructing an iterator. In our opinion, it would be negatively surprising for the same object to support both next() and containing(index), especially if index can refer to a position that has already been described by a previous iteration result, so we instead separated the two roles by defining the Symbol.iterator method on the iterable to always return a new iterator (again, just like array/set/map/string/etc.).

So the question here is whether Number.range is an iterator factory or returns an iterator factory. I personally can't imagine much use for the latter, and to me Number.range(0, 10) is much more like Array(10).keys() (a trivial iterator that basically supports only next()) than it is like Array.from(Array(10), (_, i) => i) (an object with its own state and methods that are independent of any constructed iterator(s)).

If there are no objections, I'll change it to return a Range object with [Symbol.iterator] on it later.
Wait for more discussions

Return an iterator

Current design.
Problem:

  • cannot be reused.
  • it may be strange to add to many items on an iterator prototype
Number.range(...): RangeIterator<number>

Behave like Symbol constructor

Problem:

  • maybe not subclassable
  • not match the mental modal?
Number.range(...): Range
class Range {
    [Symbol.iterator]
    get from()
    get to()
    get step()
}

A normal class

Problem:

  • Usage become unacceptable long (Iterator.from(new Number.Range(...)).take(...).toArray(...)))
class Range {
    constructor(from, to, step)
    [Symbol.iterator]()
    get from()
    get to()
    get step()
}
Number.Range = Range
BigInt.Range = Range
commented

I think I've objected to that idea a few times over at this point...

I think it's a question we should discuss at plenary, since not everyone is in agreement on this thread.

We discussed the issue in plenary, but I don't think we reached a particular conclusion. What are the next steps?

I'm going to study the ranges in other languages to make a comparison then decide which pattern is better 👀👀

I'm going to study the ranges in other languages to make a comparison then decide which pattern is better 👀👀

Interestingly, I found this re-usable problem in Rust. Rust is using the Iterator semantics. (Two for in doesn't pass the borrow checker so I manually call the next() to consume it.)

fn main() {
    let mut a = std::ops::Range { start: 3, end: 5 };
    print!("Used: {:?}\t", a.next());
    for i in a {
        print!("In loop: {:?}\t", i)
    };
}

Used: Some(3) In loop: 4

But I think manually call the next method is too explicit, and I think others try to consume the iterator twice will not pass the borrow checker. So I have no idea is this really a problem in Rust.

In my recent research of range in other languages (not completed, you can see it at https://github.com/tc39/proposal-Number.range/blob/master/compare.md). I changed my mind and now preferring to the Iterable semantics now (or other ways to keep the iterator semantics but can be re-use safely).

But before switching to the Iterable semantics, there're a few problems that we need to resolve.

Use with Iterator Helper

// Now
Number.range(0, 10).take(5).toArray()
// After
Iterator.from(Number.range(0, 10)).take(5).toArray()

I have an idea at tc39/proposal-iterator-helpers#78 (comment) but I'm not sure about it.

The naming problem

range for Iterator and Range for iterable (the idea of @devsnek)

Let it to be a class

// before
Number.range(0, 1)
// after
new Number.Range(0, 1)
// or Callable class
Number.Range(0, 1)

I have no idea if adding a new callable class like Array is acceptable today.

Let it to be a helper to create the Range class

Number.range(...) // implicitly calls new Range(...)

Another possible route: merge into Iterator namespace, becomes Iterator.range

Therefore the developers can clearly know, the return value is an iterator. And all iterators are not re-usable.

Can you elaborate on why you think it would be better to add an entire class for something that seems only likely to be used to generate a single iterator?

Can you elaborate on why you think it would be better to add an entire class for something that seems only likely to be used to generate a single iterator?

I don't like to add a class for it (the uppercase R, the new requirement to construct), but it seems like @devsnek is requiring a binding with Iterable with class.

If we want iterables for some reason the pattern you're looking for is a Number.Range class.

It is also possible to not adding a class, but a normal object with its internal slot and own prototype (for helper methods like includes). But I think if we do so, this kind of object is just a class instance without a constructor.

The iterator Number.range returns would need to be an instance of Iterator, sure, but that doesn't constrain anything about Number.range itself.

To me all that's needed is a factory function for an iterator. "iterable" is just a protocol, like "thenable" or "toStringable" or "valueOfable" or "toJSONable" - there's no "iterable" class just like there's no "jsonable" class. I continue to be confused by this direction.

Re the OP's desire is for a reusable value; if we decide that's important, I'd expect Number.range(x, y) to return a thunk for an iterator with an own Symbol.iterator method on it that's return this() - ie, a function that takes no arguments and returns a new iterator each time - so that the immediate usage was Number.range(x, y)().

Number.range(x, y) to return a thunk for an iterator with an own Symbol.iterator method on it that's return this() - ie, a function that takes no arguments and returns a new iterator each time

I like this design, it seems resolved almost all of the problems. The only problem is, is this API style consistent with other JS APIs? It seems like there is no prior API is designed like this.

If it is OK for others in this thread, I'd like to move to this API later.

No, i don't think there's any precedent for expecting iterators to be reusable in the first place :-)

No, i don't think there's any precedent for expecting iterators to be reusable in the first place :-)

Agree, I think we need to choose one of normal generator (current design) or a "generator factory"((...opts) => () => Iterator<number>). After all, an iterator is not re-useable. This problem exists for any generator functions.
And I prefer generators to classes.

After reading over this thread again, I agree with Jordan. This should just return an iterator. That's favored by ergonomics (can use immediately, no need to convert to an iterator), and matches how it would be implemented by an author (as a generator function).

The Intl.Segmenter counter-example actually has use-cases for dealing with the segments as a reified object, of which iteration is just one of the operations you can perform over it. I don't think number ranges rise to that level, so it's not worth putting together an actual object representing the range. (We don't even plan to let you measure the length of the range; you just splat it into an Array and measure the length of that.)

Getting multiple instances of the same generator is a trivial: just call Number.range() again with the same args, or write a one-liner function that does it for you. Or if you wanna get fancy, you can make a Range class that lets you configure the options as properties, and calls Number.range() for its [Symbol.iterator] method.

But the core usage is trivial and small, and favors a plain ol' generator-like implementation, returning an iterator.

@tabatkins how do you think about the latter idea Jordan mentioned? range() return an iterable generator to resolve the reusable problem

to be clear; i don't personally consider it a problem, and imo the best solution is to make it return an iterator, and for those who want reusability, they can stick () => in front of their Number.range() call.

I think Number.range(10)() is a weird and unprecedented pattern, and we shouldn't introduce it here. As Jordan says immediately above, people can just make their own arrow functions if they want an easy way to create the same range multiple times.

Again, if you wrote this yourself, the obvious way to do so is with a generator function, which would just return an iterator. We shouldn't be innovating in surprising ways here without a pretty compelling use-case, which so far hasn't been presented.

Can I think we have a consensus to use iterator semantics?

CC who seems like to support the iterable semantics: @sffc @littledan

And it's still possible to maintain the reusable by a @@iterator method on the iterator itself which will implicitly construct the range again with the same arguments (therefore return a fresh iterator for for...of Set Array.from usage).

And it's still possible to maintain the reusable by a @@iterator method on the iterator itself which will implicitly construct the range again with the same arguments (therefore return a fresh iterator for for...of Set Array.from usage).

I can imagine a possibility where I might pass the result of a range call to a function that accepts any Iterator instance. Such a function might first get some values and then consume any remaining values with either a for-of loop or Array.from. I think, for this to work as expected, the iterator's @@iterator method would need to return the this value. However, I cannot think of a plausible example at the moment, so perhaps this use case does not exist or is very rare.

Is there a precedent for a built-in iterator's @@iterator method returning anything other than the this value ⁠— I did not find one ⁠— and would it be a surprising behavior?

I'll close this to keep the non-reusable iterator semantics. I think the current semantics is okay, developer will learn to know that iterator is not reusable because every iterator behaves like this. Just like they have to know Array.sort is mutating the original array.
Happy to see if there are any further suggestions.

I still hope this could be reconsider. There are many arguments about reusable issue in iterator helpers and seems no consensus. My suggestion is we'd better not spread non-reusable semantics in the languages before we have real, solid consensus.

Note, currently there are only values/keys/entries methods return iterator directly in the languages and web apis. But they are methods with no param, so they do not have reusable issue because u can just reuse the objects.

The only exception I know is matchAll which we recently add, not sure whether it was discussed to return iterable or iterator. But at least it seems there is no many reuse use cases for string match result, and even there was reuse use case, it only have one param (regexp, mostly literal or const) and not too hard to reuse. On the other side, I feel range have many reuse use cases, and range have 2 params with an optional object options, this make the reuse much harder than matchAll.

It’s already reusable as a function; matchAll is also reusable with a string and a regex inside a function.

We don’t need to produce nouns (objects) when we have verbs (functions) available.

It’s already reusable as a function

Not sure what u mean of "reusable as a function", do u mean create a reusable closure ? Yes we could, but it add extra cost, actually it's just like creating a reusable iterable manually. If it's the common case, why we not provide an API return iterable instead of iterator?

Yes, i mean stick () => in front of it - I’m not sure what cost that adds.

I don’t actually think it’s a common case - personally i think the common case will be to make a range and use it once. But, just like every other one-use operation in the language, it has the easy composability of “put it in a function” to make it reusable. Addition isn’t reusable either, and that hasn’t stopped anyone from making reusable addition functions :-)

I'll leave this decision to the July meeting, hope we can find the correct route

I really feel there are many cost if range returns iterator.

Learning/understanding/remembering cost:

Programmers need to remember range() returns iterator and can't be reused, this is very different to range in python, _.range in lodash, range in ix (mention ix package because iterator-helpers list it as prior art)..., and I will argue that the intuition of the name "range" is it should be immutable and reusable.

Error-prone:

Even programmers have known that range() returns iterators, there are many cases we start from a simple one-time usage range like:

...
const numbers = Number.range(start, end, {step})
consume(numbers)

Nothing wrong about such usage, but with the time pass, it's possible the code become like:

import {numbers} from './common'
consume(numbers)

Now, when we reuse numbers for any reason one day, we just introduce a bug.
Note it's hard to recognize numbers is an iterator.

Such accidents could occurs in various form.

// a very correct usage!
export function f() {
  const numbers = Number.range(start, end, {step})
  return consume(numbers)
}

After careless refactoring

import {numbers} from './common'
export function f() {
  return consume(numbers)
}

Note it's likely u still could pass all unit tests of f, unless u have a test to call f() twice. And fortunately (or unfortunately), there is no production code call f() twice, so depend on what "bug" means for u, it could even not be seen as a bug at all.

Refactoring cost:

We finally find the potential issue of f() , how to fix it?

We could rewrite numbers to a closure like @ljharb suggested, but u also need to change consume(numbers) to consume(numbers()) which means u need to change all exist client code use numbers.

So a better solution is just make numbers iterable object:

export const numbers = {
  [Symbol.iterator]() { return Number.range(start, end, {step}) }
}

Another small problem is, the parameters are possibly expressions. So simply add () => or wrap it to iterable may be not efficient or even wrong, so the final code would like

const start = exp1, end = exp2, step = exp3
export const numbers = {
  [Symbol.iterator]() { return Number.range(start, end, {step}) }
}

If programmers finally write such code, they will ask why range is not iterable in first place so they can just keep simple export const numbers = Number.range(exp1, exp2, {step: exp3}) without any trouble?

I'll leave this decision to the July meeting

@Jack-Works If that, we'd better reopen this issue before we have final decision.

commented

After careless refactoring

This refactoring hazard doesn't seem realistic to me. You can make that mistake with literally any value specific to a given function invocation, being an iterator doesn't exacerbate that problem.

function x() {
  const time = Date.now();
  consume(time);
}
function add(a, b) {
  const result = a + b;
  consume(result);
}

You are continuously describing the need for reusable logic, not reusable data. Luckily, it is extremely easy to create a function in JS, so I don't think we need to worry about it.

u also need to change consume(numbers) to consume(numbers()) which means u need to change all exist client code use numbers.

That's literally a single location. If it is multiple locations that means the function code was already broken because it was trying to consume the same iterator multiple times.

@hax:
Nothing in your argument is specific to Number.range(); it's a generic argument against the concept of generators in general (or perhaps against ever exposing a generator from spec-defined functions).

If you want to argue against the core language ever using generators, that's fine (but I doubt you'll succeed). If you want to argue that generators in general are fine, but there are specific reasons that Number.range() is bad as a generator, that's fine too (but I don't see any). But you can't make a generic argument against generators and then only apply it to this specific case.

@devsnek I don't get your example, time and result in your examples seems are immutable values and can't compare to iterators.

it is extremely easy to create a function in JS

Yes it's extremely easy but only make sense if the programmers understand they need to create a function.

@tabatkins I'm arguing :

  1. Many people don't aware the difference between iterator and iterable
  2. Even they know, most will not expect range is not iterable, especially most other programming languages use iterable for range.
  3. Before this proposal we don't have builtin and web APIs returns iterators directly except values/keys/entries and matchAll.

Point 2 is specific to Number.range and point 3 is much general.

About generator, I do not argue against using generators, but more like suggesting the best practice of using generator. As my previous comments about iterable vs iterator, generators should be used for building components or ad-hoc usage, not for public api directly, if use for public api, we'd better only use it on methods with no argument, so there will be no reuse issue.

commented

@hax it's not about mutability, it's about how you can't factor logic out without wrapping it in a function. if you factor Date.now() out without wrapping it in a function you'll get the wrong time. if you factor a+b out without wrapping it in a function that also handles the values you'll get a reference error. Because this is already how js works, I think programmers have a good understanding that reusing something often requires wrapping it in a function.

@devsnek I still don't agree they are comparable. Programmers expect Date.now() returns different values on every single call. This is not true for most other things includes range. And in the example a + b, a and b is local variables, if the cases like my example (a and b are const outside the function), u could of coz fact a+b out, and u should, because compute it every time is wasting.

@hax suggest that we can add a .values() on the %RangeIteratorPrototype%. Then it can be used like

range(a, b).values().map(...).take(...).toArray()

I think this is the perfect way to resolve the ergonomic with Iterator Helpers. Therefor I'm prefer Iterable now.

What ergonomic issue? for..of works with an iterator, and the iterator helpers work with an iterator, and that's what range should return.

"Iterable" is a trait, a protocol, not a kind of object. It makes no sense to "return an iterable" because that is not a thing that exists. An iterable what? An iterable plain object? That seems bizarre to me, and there's no precedent in the language for it.

What ergonomic issue

Previously it is a blocking reason to select Iterable (of class / plain object) because Iterator.from(range(...)).map(...) is not ideal. With .values() method, the chain call make it more easy to use (if it is not iterator).

Because the iterable way no longer have ergonomic issue, I can now re-evaluate the re-usable value problem. Previous discussion have pointed out the iterator way is error proneness.

Maybe we can try to measure two kinds of designs that Yulia presented? https://docs.google.com/presentation/d/1OpKfS5UYgcwmBuejoSOBpbgsYXXzO0gG7GJHo65UXPE/edit#slide=id.g85f9dfa264_0_51

An iterable what? An iterable plain object?

Yes, iterator of plain object or iterator of a class (#22).

I'm not sure why that would be necessary; range() would of course return something that was already a proper Iterator. Either way, using Iterator.from is like Array.from or Promise.resolve, which in practice have demonstrated themselves to be quite usable - there's no ergonomics issue I'm aware of.

To be clear, I am still convinced that unless this returns an iterator directly, it should not be added to the language.

@ljharb As the https://github.com/tc39/proposal-Number.range/blob/master/compare.md#return-type , most other programming languages (we can investigate more languages if needed) do not returns an one-time consumed iterator directly.

Either way, using Iterator.from is like Array.from or Promise.resolve, which in practice have demonstrated themselves to be quite usable - there's no ergonomics issue I"m aware of.

I think @Jack-Works means writing Iterator.from(Number.range(1, 10)) is much painful than Number.range(1, 10).values(). Personally I am neutral of it. This argument is only focus on whether range is ergonomics enough to use iterator helpers.

While JS should always be informed by other languages, it is absolutely not constrained by them - new JS features should follow JS idioms, and values/keys/entries/matchAll very much define that idiom.

Again, I don't understand why Iterator.from would be required at all - I'd expect to be able to write Number.range(x, y).map(…).take(…) etc.

Again, I don't understand why Iterator.from would be required at all - I'd expect to be able to write Number.range(x, y).map(…).take(…) etc.

Yes. I'd like to do it so. Iterator.from is used to convert iterators that do not have %IteratorPrototype% to use with the helpers (or get an iterator from the iterable).

If we decided the re-usable problem is important, either we need range(...).values().map(...), Iterator.from(range(...)) or range(...)[Symbol.iterator](), either the Iterator Helpers needs to support Iterables as the "this" value.

IteratorPrototype.map = function* (f) {
    const iter = Iterator.from(this)
    // ...
}

Then, set %RangeIteratorPrototype%.[[Prototype]] to %IteratorPrototype%, we can have both re-usable semantics and range(...).map(...) conversions.

I would assume range() would return an iterator that already inherited from %IteratorPrototype%, making Iterator.from a no-op for it.

Like every builtin iterator, it would also have a Symbol.iterator method that does return this, so as to allow direct for..of usage.

new JS features should follow JS idioms, and values/keys/entries/matchAll very much define that idiom.

@ljharb

Iterators/Generators and related Ranges are very common feature in many languages, especially JS iterators/generators is inspired by python design. The core concept are very same: Iterables are objects which can be iterated, iterators are special object which delegate the iteration behavior for iterables, generators are syntax which help implement iterators in user land.

So the common cases are a method returns an object, and it happened to have implemented iterable protocol. Actually return iterators directly are special cases.

If we see values/keys/entries as "idiom" we should defined it accordingly :

  1. They are all instance methods on iterable object. (Number/BigInt.range() is static method which not match)
  2. They are all methods with no argument. (Number/BigInt.range() have three arguments)
  3. They just return plain iterators (only next() or protocol methods like @@toStringTag), no other addition (range already have additional accessors/methods)

In the context of this issue, these important natures make values/keys/entries do not have reusable issue, because u can just reuse the iterable object and nothing u can use on plain iterators except iteration. Number/BigInt.range are just the opposite.

matchAll is very new, and I don't think it stand for any "idiom" up to now, but we still could check the natures:

  1. matchAll is an instance method on iterable (string).
  2. matchAll have one argument
  3. matchAll return plain iterators

So it's more close to values/keys/entries not range.

We should also notice Intl.Segmenter intentionally choose iterable even it much close to matchAll on the second point, the main difference is it need some addition like containing method.

I think @sffc have given a very reasonable comment:

Basically, if the return value of Number.range(...) should be considered a "thing", then you should make it have its own prototype that's an iterable but not an iterator.

And I believe range is a "thing" which beyond solely iteration behavior, for example, python range is used to returns an array, but change to iterable in python3, so it's a "thing" which array like but don't need waste memory.

commented

If you think range should be an iterable please specify it as a constructor to stay consistent with our language patterns.

@devsnek Do u mean it should be new Number.Range ? Personally I don't think it's necessary , Number.range is a static method which could be a factory, but i'm ok with new Number.Range. This should be a separate issue.

commented

As per the chart I posted above it needs to be an iterable class or a function that returns an iterator. I would not be comfortable with anything else.

@devsnek

I'm not sure, is this only apply to iterable? Why only iterables need to be constructor and can't created by a static factory method? What if we have both NumberRange and Number.range()? Do u think we also need to change Intl.Segmenter?

And again, let's first decide whether it should be iterable (semantic issue) and then consider surface api problems.

And again, let's first decide whether it should be iterable (semantic issue) and then consider surface api problems.

I think the reason I feel uncomfortable with Number.range returning a reusable iterable entity is that I view it as a lightweight operation over an implicit iterable entity. The start and end arguments define an interval and, although I currently see no particular need, it would be reasonable to have a first-class Number.Interval type that implements the iterable protocol. Then Number.range would — ignoring side issues — be equivalent to

Number.range = function ( start, end, step ) {
    return new Number.Interval( start, end ).values( step ); // an iterator over the interval
};

Even without a first-class Number.Interval type, I still deem Number.range an operation in which the step argument enables me to specify how I would like to iterate over the interval.

Perhaps Number.range could return an iterator, and another proposal would consider a first-class typeNumber.Interval, Number.Sequence, or Number.Stream — that implements the iterable protocol.


Note (2022-07-07): #57 makes a similar suggestion to split the proposal into two proposals.

commented

So. When we started working on iterator helpers, it was not clear whether they would be iterator helpers or iterable helpers, and the main disagreement was around how reusability is represented in iterators and iterables. In particular people argued, like you do, that using iterators instead of iterables meant you lost reusability. The conclusion we came to however, was that in JS the primitive of reuse was not actually the iterables, but function calls. This is mainly due to the fact that "iterable" does not describe a non-scalar type that can produce an iterator, but rather any object that happens to have a Symbol.iterator property, including iterators themselves. The signs of reusability of iterators were actually one of the following: The base being an instance of some non-scalar type (like Array.prototype[Symbol.iterator]), or getting the iterator directly from an explicit function call. I want to stress that this is not just my sole opinion of how you should use iterables, but the result of research over many months by many people to understand what the best way to deal with iterators in JS should be, and it allowed us to gain consensus and move forward with that proposal. Number is a scalar type, and Number.range is a function. These two points strongly suggest that the return value should be an iterator, not an iterable.

Finally, my opinion on this specific choice: I think you should choose Number.range over NumberRange because I think function calls generally provide a better way of being explicit about reusability than class instances, and the pattern also protects you in codebase-wide reuse. For example, export let x = () => Number.range() vs export let x = new NumberRange(). The latter is just some mutable object hanging around, not very clear about what patterns it follows.

Do u think we also need to change Intl.Segmenter?

Intl is somewhat different because it uses the factory pattern for all its top level apis, unlike the rest of our stdlib. I don't agree with the design but I don't think it would be constructive to discuss changing it at this point.

@devsnek Is there any link of details about "research over many months by many people to understand what the best way to deal with iterators in JS should be" so I can learn it??

And I want to point out, we are not (only) talking about reuse iterator, but by itself, range (or interval), could be a "thing" beyond iteration.

commented

@hax

Is there any link of details about "research over many months by many people to understand what the best way to deal with iterators in JS should be" so I can learn it??

old issues about iterables in the proposal repo i guess? might be some stuff in the tc39 irc logs too.

And I want to point out, we are not (only) talking about reuse iterator, but by itself, range (or interval), could be a "thing" beyond iteration.

That's totally valid, it would just be a NumberRange constructor instead of Number.range.

Another possible real-world usage that requires reusable value: facebook/react#20707

And again, that's a general iterator issue, and wouldn't be affected by anything we do with Range in any case.

I believe this issue has been resolved by renaming it to Iterator.range (advanced to stage 2 on today's meeting). Now it's impossible to accidentally use it as a reusable value.

The problems with polymorphic functions are :

  • Not extensible => the function has to change its signature to support future types (SOLID open/close principle) Also imagine the scenario if BigDecimal is released after your proposal.
  • Not easy to polyfill (test if Number.range != null, BigInt.range != null VS "Check if Iterator.range throw an exception if my type is supported")

Polyfillability doesn’t impact proposals.

@jpolo

The motivation and the main use cases of the proposal is about simple iteration of integers (like 1 to 100), change name to Iterator.range not only solve the dilemmatic arguments of "Iterable vs Iterator" but also make the API usage and intention clearer. Actually I am not sure there are strong use cases of BigInt.range (and Decimal.range), and there are footguns of iteration floats (see #64 ), so I would ok if the proposal would eventually only support safe integers (and renaming to Iterator.integers), and never extensible to future types.

On the other side, when we went to Iterator.range, Jack and I also discussed possibility of extend to any type which implement "range" protocol (like Stride protocol in Swift). If we really want this API be "open" to any future types, I think protocol is the right way (not adding range method to every new type), and protocol would also easy to polyfill.

Closed and all but had to drop this
A way to decide whether Number.range should return an iterable or an iterator is to examine after it is called:

  • Iterable
    Returning an iterable such as new Map(...).entries(), [].values() offers the gain of dynamically generating an array of arbitrary length and the loss of having to wait until the iterable is ready before trying to .map, .filter etc, them

An allternative may be to provide room for a callback for each yielded value

  • Iterator
    Offers a lot of room for creativity and improvement while allowing yielded values to be used with no delay. However, you have to ask, do range in Python, which may be an inspiration for this proposal, work this way? If not, what are the gains of this approach as regards the expected ergonomics of Number.range

Renaming it to Iterator.range only subtly hides this conundrum

.entries() and .values() return iterators (which happen to be iterable). There's no such thing as a generic "iterable", so you'd have to define a "thing" that happens to be iterable to return.

@ogbotemi-2000 Yes, python (and most programming languages) range api return re-useable iterables not one-shot iterators. Personally I would like the API follow python. Unfortunately it's very controversial in the committee. Renaming is the best and tradeoff I can tell, or we will never advance.