tc39 / proposal-iterator.range

A proposal for ECMAScript to add a built-in Iterator.range()
https://tc39.es/proposal-iterator.range/
MIT License
487 stars 13 forks source link

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

Closed rbuckton closed 1 year ago

rbuckton commented 4 years ago

In https://github.com/tc39/proposal-slice-notation/issues/19#issuecomment-415995994 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).

Jack-Works commented 4 years ago

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)

tabatkins commented 4 years ago

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.

rbuckton commented 4 years ago

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).

Jack-Works commented 4 years ago

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)

tiansh commented 4 years ago

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

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.

hax commented 4 years ago
  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?

littledan commented 4 years ago

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.

gsathya commented 4 years ago

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?
Jack-Works commented 4 years ago

(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)

gsathya commented 4 years ago

(I think this belong to the slice notation proposal)

Why?

Jack-Works commented 4 years ago

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

gsathya commented 4 years ago

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]
obedm503 commented 4 years ago

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.

mpcsh commented 4 years ago

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.

ljharb commented 4 years ago

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

Andrew-Cottrell commented 4 years ago

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.

mpcsh commented 4 years ago

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).

Andrew-Cottrell commented 4 years ago

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

Jack-Works commented 4 years ago

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.

hax commented 4 years ago

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.

stiff commented 3 years ago

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']];
Jbnado commented 1 year ago

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.

Jack-Works commented 1 year ago

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