dart-lang / collection

The collection package for Dart contains a number of separate libraries with utility functions and classes that makes working with collections easier.
https://pub.dev/packages/collection
BSD 3-Clause "New" or "Revised" License
373 stars 87 forks source link

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

Open Sunbreak opened 3 years ago

Sunbreak commented 3 years ago

Ref: https://github.com/dart-lang/sdk/issues/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 commented 2 years ago

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 :stuck_out_tongue:.

lrhn commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 1 month ago

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 https://github.com/dart-lang/language/issues/3366.