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

Consider first-class type (i.e. 'NumberRange' or 'Interval') vs. a 'range' method

rbuckton opened this issue · comments

In tc39/proposal-slice-notation#19 (comment) I suggested adding something similar to the C# Index and Range types which could also address these use cases with a more flexible API than an overloaded Number.range method.

An example of these APIs can be found here:

The advantage of this (or a similar) API is that it provides a convenient place to attach functionality like .includes(value), as well as a place to provide different ways to generate an Interval without relying on possibly confusing overloaded behavior like Number.range(10).

I'm trying to avoid adding a global name on the globalThis but if everyone is okay with that, I'm also okay to change the API design.

On the other hand, the slice notation can behave the following way

// Literal `Range` syntax:
let range = 1:3; 
// -> range = Number.range(1, 3);

And I will add @@slice, @@geti on the return value. (Currently, it is immutable so @@seti won't be involved)

I'm opposed to a syntax that would require a new just to create a range; it's visual noise that distracts from what should be a lightweight operation. If the function can be called without a new, then I have no opinion on whether it's an object or just a plain iterator.

There's no reason Interval(1, 3) couldn't be synonymous with new Interval(1, 3). If we had value types, I imagine something like Interval would be a value type (in which case the new would be unnecessary/would result in boxing like new Boolean vs Boolean).

Value type for range? Do you mean the record and tuple or a separate new value type? (I don't think it should be a value type)

If so, I would prefer NumberSequence or NumberStream. So you can make functions like:

  • NumberSeq.range(from, to, step = 1), NumberStream.range(to);
  • NumberSeq.count() // output [0, 1, 2, 3, ...]
  • NumberSeq.iterate(initial, iterateFunc) // output [initial, iterateFunc(initial), iterateFunc(iterateFunc(initial)), ...]`
  • NumberSeq.repeat(seed) // output [...seed, ...seed, ...seed, ...], or [seed, seed, seed, ...]
  • ...

But not need to limit to NumberRange.

We can also make the class work for non-numeric types, and rename it as Sequence or Stream. This may be too far from what this spec want to do. But API like this make more sense to me.

  1. I like the idea of being value type. It allow us use === (for example, ^1 === ^1 could return true if Index is value type)

  2. I also like the idea of range.includes(), but it doesn't require range to be value type, so it could be a separate issue. I believe #17 already cover that.

  3. I think we still need separate classes (or value types) for different range (interval), aka. NumberRange, BigIntRange, IndexRange. Note they could be Number.Range, BigInt.Range, Index.Range so we don't need adding too many names to globalThis.

  4. Consider possible range literals, (1n:3n) should be BigInt.Range(1n, 3n), (1:^1) should be Index.Range(Index(1), Index(1, {fromEnd:true}), but what about (1:3), whether it create Number.Range or Index.Range?

Value types are pretty far out. If we had them now, it could make sense. I think this proposal fills an urgent need, and we shouldn't wait on value types for them. I think this proposal will be very very useful with range iterables (if we have them at all) being objects. Most of the time, you won't really use the iterable much at all; you'll just get its iterator. The Temporal proposal is making a similar tradeoff.

What would happen on property access with this new built in type?

const r = Range(1, 3);
[1, 2, 3, 4, 5][r] 
// [2, 3] or undefined?

(Note your code have ASI problem)

I don't think we should overwrite the behavior of [ ]. (I think this belong to the slice notation proposal)

(I think this belong to the slice notation proposal)

Why?

Maybe things like [r.from:r.to] or other notation. I'm not preferring to overload the [expr]

I'm not preferring to overload the [expr]

I agree.

I'm not sure we'd want different behavior for slice notation depending on where it's used. Not a fan of this:

const r = 1: 3; // creates a Range object
[1, 2, 3, 4, 5][r]; // undefined
[1, 2, 3, 4, 5][1: 3]; // [2, 3]

I'd prefer this:

const f = 1:3; // SyntaxError
const r = Range(1, 3);
[1, 2, 3, 4, 5][r]; // undefined
[1, 2, 4, 5, 5][r.from: r.to]; // [2, 3]
[1, 2, 3, 4, 5][1: 3]; // [2, 3]
commented

Perhaps as part of a different proposal, it would be good to introduce some sort of getter/setter protocol. If Array.prototype[Symbol.getter] exists, it is called and gets passed whatever is between []. If not it does regular property access. It add more extensibility to the language.

late to this issue, but wanted to re-up the notion of a .includes method. I'm opposed to the idea of reusing a range, but I think that's not necessarily a problem; for example, Rust's iterator semantics allow for a .includes method, which simply consumes the iterator. I don't see why we couldn't do that here, as a way to allow this convenience method without cracking open the reusability discussion.

EDIT: striking discussion on reusability, as I think it's actually just orthogonal to this issue. regardless, curious to hear thoughts on .includes specifically.

I'm not sure what you mean by ".includes" - you mean a method on the iterator itself?

curious to hear thoughts on .includes specifically.

What would be the semantics of an includes method? Would it consume the iterator? Might it only compare a searchValue with the range endpoints, or should it take the step value in to account and return true only for a searchValue that would be yielded? Would it, like Array#includes, use SameValueZero equality?

proposal-iterator-helpers has a spec for %Iterator.prototype%.some. I think it would be least surprising if an iterator's includes method — wherever specified — were functionally equivalent to

proto.includes = function (searchValue) {
    return this.some(value => SameValueZero(value, searchValue));
};

If the semantics are likely to be different than those that might be specified elsewhere, it may be preferable to consider using a different name for the suggested method.

I'm not sure what you mean by ".includes" - you mean a method on the iterator itself?

yep, like Number.range(0, 10).includes(5)

What would be the semantics of an includes method? Would it consume the iterator?

I think yes, it would have to consume the iterator.

Might it only compare a searchValue with the range endpoints, or should it take the step value in to account and return true only for a searchValue that would be yielded?

I could see this going either way. should Number.range(0, 10).includes(5.5) return true? I'm not sure on which side I fall there.

Would it, like Array#includes, use SameValueZero equality?

I think so, yes.


I actually wasn't aware of the iterator helpers proposal - %Iterator.prototype%.some does seem like a reasonable possibility here, in which case there's nothing to be added to this proposal. but would the other semantic - i.e. simply checking that the search value lies within the bounds of the iterator, with no regard for the step size - be useful? if so, perhaps that would be useful here (and perhaps under a different name).

but would the other semantic - i.e. simply checking that the search value lies within the bounds of the iterator, with no regard for the step size - be useful? if so, perhaps that would be useful here (and perhaps under a different name).

I think it would be slightly more useful to instead have something like

Math.inInterval = function (number, inclusive, exclusive) {
    if (inclusive < exclusive) {
        return inclusive <= number && number < exclusive;
    }
    return exclusive < number && number <= inclusive;
};

This, or something similar, could be specified in https://github.com/rwaldron/proposal-math-extensions

We would then have

var range = Number.range(0, 10);
Math.inInterval(5.5, range.start, range.end); // true

Note (2022-07-14): there is a proposal for a similar function at https://github.com/js-choi/proposal-math-between

This, or something similar, could be specified in https://github.com/rwaldron/proposal-math-extensions

Yes, I agree, but the includes that will consume the iterator can be also specified in the iterator helper proposal.

Consider slice notation seems have trouble to advance, I think we could reconsider the ideas of range[i] or range.item(i) (random access like array, at least python range has that). Note current semantic allow us have O(1) performance and don't rely on iterator. Similarly, includes(x) could also be O(1) and don't rely on iterator.

commented

What would happen on property access with this new built in type?

Sometimes it is very handy to get range of array, but overloading [] is too much, maybe it would be nice for Interval/range to be callable, like:

const rows = [ ['a','b','c','d'], ['d','e','f','g'], ['q','w','e','r']];
(1..2)(rows) == [['d','e','f','g'], ['q','w','e','r']];
rows.map(1..2) == [ ['b','c'], ['e','f'], ['w','e']];

I think a range should be in Number, because we are talking about use a range of numbers, create a new class of Interval could be too much just to a range.

looks like a first-class value is very far from us... closing this for now but still welcome discussions