Skip to content

Add Python-style range() function and iterable #624

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

Open
Sunbreak opened this issue Sep 2, 2021 · 6 comments
Open

Add Python-style range() function and iterable #624

Sunbreak opened this issue Sep 2, 2021 · 6 comments

Comments

@Sunbreak
Copy link

Sunbreak commented Sep 2, 2021

Ref: dart-lang/sdk#7775

extension IntRange on int {
  /// A arithmetic sequence of numbers from [this] to, but not including [end].
  /// 
  /// The difference between two consecutive numbers is [stepSize].
  /// The [stepSize] must be positive (not negative or zero).
  ///
  /// The first number is always [this].
  /// If [this] is smaller than [end], the next number in the sequence is found by
  /// adding [stepSize] to the current number until reaching or exceeding [end].
  /// If [end] is smaller than [this], the next number in the sequence is found by
  /// subtracting [stepSize] from the current number until reaching or subceeding [end].
  Iterable<int> to(int end, [int stepSize = 1]) sync* {
    RangeError.checkValueInInterval(stepSize, 1, null, "stepSize");
    if (start <= end) {
      for (var i = this; i < end; i += stepSize) yield i;
    } else {
      for (var i = this; i > end; i -= stepSize) yield i;
    }
  }
}
@lrhn
Copy link
Member

lrhn commented Jan 3, 2022

This is definitely the right place for such an extension, should we want to add it, since it creates an iterable.

We've considered such "range" functions on int before, and it's always been a problem that it's unclear whether the end point is included or not. Adding both to int seemed overkill.

With extensions we could possibly have both, say 2.to(4) and 2.until(5).

The implementation is not particularly efficient, unless the compiler recognizes and optimizes it.
You'd probably always be signficantly better off, performance wise, doing for (var i = start; i < end; i += step) ... than for (var i in start.until(end)) ....

That's what worries me the most: Adding this might make people think using it is better than a normal for loop. It won't be. Shorter, possibly, but definitely slower.

Which prompts the question: When would you use such an iterable?
The for loop is the place I usually see suggested, and that's exactly where I wouldn't recommend using it.
The other example in #7775 was List.from(start.until(end)), where you should now write the more efficient [for (var i = start; i < end; i++) i].

So, in the currently proposed use-cases, I wouldn't actually recommend using this feature.
(Again, not unless we promise that compiles will convert for (var i in start.until(end)) into for (var i = start, n = end; i < n; i++) - and that only gets more complicated if we allow stepping down as well as up.)


Worst-case syntax strawman: 2[4] (operator[]) is inclusive, 2(5) (call method) is exclusive 😛.

@lrhn
Copy link
Member

lrhn commented Jan 3, 2022

If we didn't have list-comprehensions, I'd agree more.

var squares = [for (var x in list) x * x];

That said, we are doing for-in iteration over lists all the time, and because List is an interface with an infinite number of implementations, not all of those can be optimized into index based loops.
So, performance isn't necessarily the only thing that matters.

And if anything, a static method creating a known Iterable/Iterator is probably easier to optimize than for-in on a List.

Then there is the issue that 2.to(5) is ugly. Why is 5 in parentheses, and 2 is not? Why is the first value special? (Answer: because we are pretending to be a method on the first value, even though the operation is really symmetrical.)

We could have Range(2, 5) already, but nobody's asking for that. Why not? It's no longer than 2.until(5).

Or just have Iterable<int> get ints sync* { for (var i = 0;; i++) yield i; } and do ints.take(10) and ints.getRange(2, 5).

It's almost like people want something smaller and without unnecessary characters. Like, native syntax for ranges!

Languages with syntax for ranges exist and are happy! Python's [x:y] is a valid approach. Doesn't have an end-exclusive version though. Awk/Perl's x..y (end-exclusive) and x...y (end-inclusive) ranges are also great, buy x..y is taken in Dart.

@lrhn
Copy link
Member

lrhn commented Jan 4, 2022

Hmm. The even easier version is to make int itself iterable. Effectively making integers into von Neumann ordinals of themselves. Well, technically they should be sets.

for (var i in 5) print(i); // 0, 1, 2, 3, 4

That's probably not going to fly since Iterable has so many methods (5.contains(4), 5.first == 0, 5.last == 4 ... all look odd).
Would probably count down from zero for negative numbers.
If we allow for/in iteration to be structural (just have an Iterator get iterator rather than needing to be an Iterable), possibly even an extension get iterator, then this becomes much more likely. (The sad thing about that is that the best name for an interface with just get iterator would be ... Iterable).

Anyway, maybe a getter, n.range, which gives an immutable set of the non-negative integers below n. Counts down for negative numbers. Allows operator+(int offset) to shift it. So 5.range + 2 is {2,3,4,5,6}, (-5).range + 4 is {4,3,2,1,0}.

extension IntRange on int {
  Range get range => Range(this);
}
class Range extends UnmodifiableSetBase<int> {
  final int _count;
  final int _offset;
  Range(int count) : _count = count, _offset = 0;
  Range._(this._count, this._offset);
  Iterator<int> get iterator => () sync* {
    for (var i = 0, delta = _count < 0 ? -1 : 1; i != _count; i += delta) yield (i + offset);
  }().iterator;
  bool contains(Object? n) => 
      n is int && _count < 0 ? (_count < n && n <= 0) : (_count > n && n >= 0);
  Range operator+(int offset) => Range._(_count, _offset + offset);
}

and, voila, you can do

for (var i in 5.range) { ... i is 0..4 ... }
for (var i in 5.range + 2) { ... i is 2..6 ... }

Not very readable, though.
Maybe 5.count? And use 7.count.skip(2) for 2..7?

@thomassth
Copy link

This is very confusing to understand

5.count reads like 5 gets counted , instead of counting from 0 for 5 times.

Maybe 5.countUntil?

@Sunbreak
Copy link
Author

FYI: Google has a quiver utility library

quiver.iterables

concat, count, cycle, enumerate, merge, partition, range, and zip create, transform, or combine Iterables in different ways, similar to Python's itertools.

@FMorschel
Copy link

FMorschel commented Aug 16, 2024

From lrhn's comment above:

It's almost like people want something smaller and without unnecessary characters. Like, native syntax for ranges!

I think it's probably a good thing to link dart-lang/language#3366.

@mosuem mosuem transferred this issue from dart-archive/collection Oct 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants