Open Sunbreak opened 3 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:.
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.
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?
This is very confusing to understand
5.count
reads like 5 gets counted , instead of counting from 0 for 5 times.
Maybe 5.countUntil
?
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.
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.
Ref: https://github.com/dart-lang/sdk/issues/7775