Skip to content
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

Improved loop constructs and scoping. #171

Open
lrhn opened this issue Jan 10, 2019 · 6 comments
Open

Improved loop constructs and scoping. #171

lrhn opened this issue Jan 10, 2019 · 6 comments
Assignees
Labels
feature Proposed language feature that solves one or more problems

Comments

@lrhn
Copy link
Member

lrhn commented Jan 10, 2019

The Dart loop constructs, for/while/do-while are fairly simplistic and low-level. There are a number of improvements that could make them easier to use to write understandable code in less convoluted manners.

Else-branches

A fairly often occurring issue is to look for something in a list, and then do something if nothing is found.
Example:

var particularElement;
for (var e in someList) {
  if (predicate(e)) {
    particularElement = e;
    break;
   }
}
if (particularElement == null) return;
useElement(particularElement)

Here we need an extra check (sometimes an extra found boolean if we can't use null to represent a missing match).

I sometimes write that with a break instead:

var particularElement;
found: {
  for (var e in someList) {
    if (predicate(e)) {
      particularElement = e;
      break found;
     }
  }
  return;
}
useElement(particularElement);

If a loop construct allowed an else branch, then it could be written as:

var particularElement;
for (var e in someList) {
  if (predicate(e)) {
    particularElement = e;
    break;
 }
} else {
  return;
}

The else branch is executed only when the loop condition becomes non-true. It is skipped over when breaking out of the loop.
(Example in dart:convert: https://github.com/dart-lang/sdk/blob/master/sdk/lib/convert/json.dart#L343).

The labeled-break based rewrite also suggests a possible desugaring.

Extended Scope of do

The do loop is sometimes exactly what you need when you want to do some computation before doing the first test. However, any variable used in the test must be declared outside the loop.

var variable;
do {
  something();
  variable = someComputation();
  other();
} while (variable.isFine);

That's annoying when the variable is not needed after the loop.

Instead we should extend the variable scope of the do body to also cover the condition, so the above can be written as:

do {
  something();
  var variable = someComputation();
  other();
} while (variable.isFine);

This breaks with the rule that dart scopes and blocks are the same thing. It means that the condition is evaluated in the scope of the block (and it's always a block, we add a block in the specification if the body is a non-block statement to avoid degenerate cases like do var x = foo(); while (x)), even if it is lexically outside. It makes sense if we see the entire do-loop as a single construct.

(Example in dart:io: https://github.com/dart-lang/sdk/blob/master/sdk/lib/io/stdio.dart#L66)

It also breaks the rule that a continue is like a break of the loop body block. or at least, only variables declared prior to any continues can be visible in the condition if a continue jumps right to the condition.

Combined do-while Loops

Another common issue is the fencepost problem. You want to do something in a loop, and then do something between rounds. This is often written as:

while (true) {
   doSomethingAlways();
   if (!test) break; 
   doSomethingBeween();
}

If we extend the do/while loop to allow two blocks, combining the do and the while loop, we can write this much more readably as:

do {
  doSomethingAlways();
} while (test) {
  doSomethingBetween();
}

This simply generalizes do-while and while loops into one that has an optional do part and a potentially empty while part.

Here we should again let the scope of the first block extend through the condition and into the second part, because it again makes the construct easier to use. It also works well with the obvious desugaring.

The scope of the do part may also be able to extend to an else part since it's definitely executed (although I'm not sure where a continue in the first, "do", part would go, because going to the condition isn't safe then).

(A continue in the first part will skip to the test. A continue in the second part will go back to the start of the loop. In both cases they work exactly like they are breaking the current block).

(Example from dart:collection: https://github.com/dart-lang/sdk/blob/master/sdk/lib/_internal/js_runtime/lib/collection_patch.dart#L656)

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label Jan 10, 2019
@lrhn lrhn self-assigned this Jan 10, 2019
@munificent
Copy link
Member

The do loop is sometimes exactly what you need when you want to do some computation before doing the first test. However, any variable used in the test must be declared outside the loop.

O_o. I didn't even know Dart worked that way. I assumed the scope would cover the condition.

This breaks with the rule that dart scopes and blocks are the same thing.

Is that a rule anyone knows? I think we will likely end up relaxing this if we do some kind of lightweight pattern-matching if statement:

Object o = ...
if (int i = o) {
  print(i.isEven);
}

So I think it's reasonable to extend the scope for do-while too.

@rakudrama
Copy link
Member

rakudrama commented Jan 10, 2019

One thing I have needed is counting iterations and detecting the first and last iteration.
Can we make that a language feature?
Counting and first iteration could be done by injecting a variable.
Last iteration of for-in could be done by calling moveNext immediately after capturing 'current'.

Something like this...

for (var e in items) {
  if (!for.first) write(', ');
  if (for.last) write('and ');
  write(e);
}
void printList(header, footer, items) {
  for (var item in items) {
    if (for.first) print(header);
    print('${for.count}. $item');  // for.count is 1-based, for.index is 0-based.
    if (for.last) print(footer);
  }
}

We could use a loop label to access iteration properties but I'd hate to have to go back and name the loop unless we are in a situation where we would name it anyway for break/continue (e.g. if (OuterLoop.first) ...). I don't like statement labels on loops because they bury the main keyword of the labelled construct.

A very simple improvement would be a simple keyword for while (true).
Middle-exits feel less forced when I don't have to write a dummy condition.
Perhaps do-while minus the while.

The issue I have with labels hiding the for also applies to if (test) hiding the break. We could make the condition a subordinate to the action:

do {
  doSomethingAlways();
  break if test;
  doSomethingBetween();
}

Now the keyword-colored do and break stand out and there are fewer extra tokens.
I think this looks cleaner than do-stuff-while-stuff and is easy to edit to add another condition.

@zoechi
Copy link

zoechi commented Jan 11, 2019

Something like this...

for (var e in items) {
  if (!for.first) write(', ');
  if (for.last) write('and ');
  write(e);
}

What about

for (var e in items; int index) {
  if (index == 0) write(', ');
  if (index == items.length) write('and ');
  if (index % 2 == 0) {
    write('even');
   } else {
     write('odd');
  write(e);
}

@l7ssha
Copy link

l7ssha commented Jan 11, 2019

In Dart we should be able to loop over maps.

for(var key, var value in someMap) {
  // Stuff
}

Loop over list with index

for(var index, var value in someList) {
  // Stuff
}

As in #68 (comment), if the dart gets support for language level tuples looping over maps will produce tuple of (key, value), which can be deconstructed into two variables in process. This is also applicable for looping over list with index.

for(var index, value in someList) {
  // Stuff
}

@nex3
Copy link
Member

nex3 commented May 16, 2019

@l7ssha's proposal would be especially useful in combination with collection-for. With that, you could (for example) write this mapMap() call like this:

var closure = transitiveClosure({
  for (var name, package in packages)
    name: package.dependencies.keys
});

Right now, there's not an elegant way of doing that with collection-for.

@a14n
Copy link

a14n commented May 17, 2019

@nex3 you can write:

  var closure = transitiveClosure({
    for (var e in packages.entries)
      e.key: e.value.dependencies.keys
  });

but I agree that naming key and value is better for understanding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

7 participants