-
Notifications
You must be signed in to change notification settings - Fork 211
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
Loop counters #2305
Comments
I think this is better solved by records, and would make interacting with lists / maps similar. for (final (i, value) in list.enumerate) {
print('index $i value $value');
}
for (final (key, value) in map.enumerate) {
print('index $i value $value');
} |
I think I'm with @TimWhiting. I agree that sometimes user want an index in addition to the element with a var elements = songs();
for (final index = 0; i < elements.length; i++) {
final song = elements[i];
if (index > 0 && index % 5 == 0) print('');
print('${index + 1}. ${song.name}');
} Now the variables I suspect that your proposal (though I do like how it looks) would be too much machinery to add to the language relative to the number of uses it would address. Even when it does come into play, I imagine whatever we added to the language would still end up being too limited and rigid. Maybe you want it to be 1-based, or you want to skip every other element, etc. Records, destructuring, and extension methods give us a nice way to have a single loop that binds a handful of fresh variables in each iteration and uses whatever logic a user desires to define those variables: extension on Iterable<T> {
Iterable<(int, T)> get indexed sync* {
var index = 0;
for (var e in this) {
yield (index, e);
index++;
}
}
Iterable<(int, T)> get indexed1 sync* {
var index = 1;
for (var e in this) {
yield (index, e);
index++;
}
}
Iterable<(T, T)> get pairs sync* {
T? first;
var hasFirst = false;
for (var e in this) {
if (hasFirst) {
yield (first as T, e);
} else {
first = e;
}
hasFirst = !hasFirst;
}
}
}
main() {
var letters = ['a', 'b', 'c', 'd'];
for (var (i, e) in letters.indexed) {
print('$i: $e');
// 0: a
// 1: b
// 2: c
// 3: d
}
for (var (i, e) in letters.indexed1) {
print('$i: $e');
// 1: a
// 2: b
// 3: c
// 4: d
}
for (var (e1, e2) in letters.pairs) {
print('$e1 - $e2');
// a - b
// c - d
}
} Etc. |
Loop counters
TL;DR: Some loops would be easier to write and be more efficient if the language took care of some simple iteration counting tasks for us.
This issue is an expansion of #171 (comment)
Background and Motivation
It is somewhat common to iterate over a collection but also need to know the position of the current element. We will use as a running example the task of printing a play list of songs, prefixing each song with the number of the song in the list, and printing a blank line between every 5 songs.
Example output:
One might count the songs as they are iterated:
This works, but there is a hazard because
index
andsong
have different scopes. There is oneindex
variable, but a freshsong
variable for each iteration of the loop. If the printing is deferred in a closure, they behave differently:The scoping hazard can be avoided by copying the index into a variable in the loop scope:
One might copy the songs to a temporary list:
This solves the scoping hazard, but is wasteful in copying to a list, and a indexing list feels like a lot of work. This is a good approach when the collection is already a List and there is no chance of it being changed to an Iterable, and when iterating several parallel lists.
Another approach is to use the functional style of chaining Iterables. There are some clever tricks that are useful; one trick is for converting an Iterable into (index, element) pairs is to convert the Iterable to a List and use the
asMap()
view of the list:The trouble with this style, and
.forEach
in particular, is that it is not easy to tweak the iteration in the 'body' of the loop. For example, if the specification is changed to stop after printing "Goodnight Sweetheart Goodnight", we have to do some kind of filtering step that is quite difficult.One can sometimes mix paradigms to use a for-in loop over a functional-style iterable:
In general, though, the functional style can be a bit inflexible, and can be less efficient as it needs intermediate Iterables and their Iterators.
Proposal
Each loop can track the index of the current iteration. This is available as a read-only
index
property of the loop. The loop is identified by its keywordfor
:The loop is identified by the keyword, so a
while
loop would have awhile.index
property.The keyword identifies the innermost containing loop with that keyword. Outer loops can be selected by using the label, similar to
break LABEL
andcontinue LABEL
.Using labels might be syntactically ambiguous.
We can drop using labels in favor of either (1) ignoring the issue of outer loops and using a work-around of copying the loop property:
or (2) allow a path upwards through the loops, so
for.while.first
would be thefirst
property of the innermostwhiler
loop that encloses the innermostfor
loop of the expression. (This upwards-navigation feels a bit backwards to me at the moment).Extra loop properties
It might be convenient to have other properties of a loop.
counter
While the
index
is useful for zero-based logic, it is sometimes convenient to have a 1-based value.for.counter
=for.index + 1
first
Some logic, like adding a separator in
join
, needs only to distinguish the first iteration from the rest.for.first
=for.index == 0
.The front-end of the compiler could decide to use a boolean flag if there is only a
for.first
property. An optimizing back-end might be able to reduce a counter with a fixed test forfirst
to a boolean.last
It is possible to recognize the final iteration of a for-in loop by rotating the
moveNext()
call earlier.behaves like
Scope
The loop properties are loop scoped, so each iteration has a fresh property (like variables declared in
for
loops). The loop properties are in scope in the bodies of loops, the expression ofwhile
anddo-while
loops, and in the condition and increment offor
-loopsControl flow collections
A enhanced collection literals have for loops, so the loop counter should be available in positions analogous to the statement version of for loops.
TBD: should be possible to label these loops in enhanced collections for disambiguation.
General path counting
Loop path counting can be generalized. Loops have an entry and an interior and a loop count is just the number of times the interior is reached after the reference point of the entry. Any point in the program can be counted with respect to a reference point.
We might use the syntax reference
.here.index
for the counter for reaching the expressionhere
subsequent to reference. The following would print a subset of songs with their original indexes, but still grouped in fives.General path counting allows the counting of inner loops with respect to outer loops.
We will probably want some convenient syntax for the reference point of the beginning of the method, perhaps
here.
ifhere
is not already an in-scope variable.A special-case starting point for counting is the beginning of the program. This could be indicated by using the
static
keyword. The expressionstatic.here.index
could be abbreviated tostatic.index
.Sequential indexes no longer require a explicit variable:
⟶
First-use initialization could be guarded with
static.first
.⟶
Related work
JavaScript examples
JavaScript's
Array.prototype.forEach
function passes three arguments to the'callback' function. Since JavaScript functions ignore extra arguments,
forEach
can be called with a callback that has one, two to three parameters.The two-argument form is useful in the same ways descibed above.
The three-argument form is useful for updating the array when
arr
is an expression that is not a variable.Report generators
Report generators typically have ways to describe how items of the report are separated, grouped or paginated without the user needing to implement those features.
Templating languages
Angular allows the index to be bound to a variable via
index as ...
:Django automatically populates several variables, e.g.
forloop.counter
,forloop.first
,forloop.last
.https://docs.djangoproject.com/en/4.0/ref/templates/builtins/#for
Django implements
forloop.parentloop
to get to the enclosing loop, e.g.forloop.parentloop.counter
. This might be a reasonable alternative to using a labeled statement.General comment on temporaries
An automatic loop counter feature is similar to many small languages features that provide a convenient and readable shorthand for something that could be achieved with a temporary variable.
By avoiding the need for a temporary variable, the programmer's flow is not interrupted by the need to move to a new location to declare the variable.
Dart has many example of small features that make the language comfortable, for example
e1 ?? e2
.In the discussion on #171 some suggestions are for declaring a index variable. I'm not in favor of that, since it breaks programming flow as you have to go back and declare the index.
The text was updated successfully, but these errors were encountered: