dart-lang / language

Design of the Dart language
Other
2.67k stars 205 forks source link

Support `continue` inside collection `for` expressions #4139

Open rrousselGit opened 3 weeks ago

rrousselGit commented 3 weeks ago

Hello!

Currently, [for (...) item] doesn't support the continue keyword. Although we can express everything without it, this can lead to a lot of nesting.

The continue keyword is a very useful tool to unwrap some amount of nesting. For example:

for (final item in list) {
  if (condition) {
    doSomething();
  }
}

// vs:

for (final item in list) {
  if (!condition) continue;
  doSomething();
}

It'd be cool to support continue inside collections too:

final list = [
  for (final item in [1,2,3])
    if (item < 2) continue
    item,
]
hydro63 commented 3 weeks ago

This is possible, but it isn't very useful imo. You can just as easily do array.where(...).map(...), and it would have the same functionality. The true power of continue can be seen when you have multiple statements, which the current list comprehension doesn't allow.

For clarification, i'm not saying continue is bad, just that it is insufficient. There is already a proposal for blocked expression, which would allow you to write the same thing, and a lot more. Another way to do exactly that is to have for as expression in Dart (which imo is a lot better than list comprehention).

// for as expression
// for expression returns iterable, yield passes the value to iterable

final list = [
  firstElement,
  ... for(final item in [1,2,3]){
    if(item < 2) continue;
    doSomething();
    doSomethingElse();
    yield item + 3;
  },
  lastElement,
];

// for -> Iterable<int> -> List<int> -> retList
var retList = for(final item in [1,2,3]){
  if(item < 2) continue;
  doSomething();
  doSomethingElse();
  yield item + 3;
}.toList();

But then again, both list comprehention and for expressions are just arr.map under the hood, so i don't know how useful it really is.

rrousselGit commented 3 weeks ago

This is possible, but it isn't very useful imo. You can just as easily do array.where(...).map(...), and it would have the same functionality. The true power of continue can be seen when you have multiple statements, which the current list comprehension doesn't allow.

By that standard, we never need [for]/[if] :) Similarly, multiple statements could be expressed with .expand

I agree that multiple statements in for would be nice. But that's a separate feature.

mateusfccp commented 3 weeks ago

For this to happen, we would have to change dramatically how collection-if work. Currently, they expect an expression. We would have to have an expression in the form if (<condition>) continue <expression>, or we would have to accept non-expressions on collection-ifs, which would be even more complex IMO.

As for my personal opinion (because this issue is based on subjective perceptions),

for (final item in list) {
  if (condition) {
    doSomething();
  }
}

Is better than:

for (final item in list) {
  if (!condition) continue;
  doSomething();
}

Also, I can't see how

final list = [
  for (final item in [1,2,3])
    if (item < 2) continue
    item,
]

is better than

final list = [
  for (final item in [1,2,3])
    if (item >= 2) item,
]
mateusfccp commented 3 weeks ago

You can just as easily do array.where(...).map(...), and it would have the same functionality.

The idiomatic way to do this in Dart is to use collection-if/collection-for instead of collection methods, so it just makes sense for the language to be more expressive in this regard.

hydro63 commented 3 weeks ago

The idiomatic way to do this in Dart is to use collection-if/collection-for instead of collection methods, so it just makes sense for the language to be more expressive in this regard.

I understand the reasoning, but i don't really think that expanding what collection-for can do is the way to go. As i said, i think that for as expression would be a lot better construct, than the python style list comprehention we have.

Still, since collection-for is already part of Dart, i guess it is already decided.

rrousselGit commented 3 weeks ago

We would have to have an expression in the form if (<condition>) continue <expression>

That's what I'm asking, yes.

For a simple example, the difference may not be big. But you can technically chain if/for multiple times

For example, we could do:

if (!a) continue
if (!b) continue
for (...)
  <expr>

as opposed to:

if (a)
  if (b)
    for (...
      <expr>

Widgets are already quite nested.
DartFmt is getting reworked in large part for the sake of removing levels of indentations inside widgets. IMO there's value here.

Levi-Lesches commented 2 days ago

I agree that there's value to a short if (!something) continue as opposed to wrapping a possibly complex expression in an if. Can't say I haven't wished for this feature a few times myself

tatumizer commented 2 days ago

Rather than adding more and more syntax, dart could optimize Iterable methods (where, map and friends) - e.g by inlining them whenever possible - so you would never need anything beyond

[...[1, 2, 3]
  .where(# < 2) // no extra indentation
  .map(# + 10)  // no extra indentation
  .etc...
]

(You can do it today, albeit with a huge hit in performance).

rrousselGit commented 2 days ago

@tatumizer Dart added collection expressions for a reason.

Using iterables with widgets both has horrible readability and is a lot more limited.

lrhn commented 1 day ago

As pointed out, the way elements work, they are generally in tail position, so you don't need a continue, just an empty branch.

The proposal is to introduce a new syntactic form

  if (expression) continue element

which is really a "shorthand" syntax for if (!(expression)) element, and with an accompagnying formatting that makes it look like

  if (expression) continue
  element

Doesn't seem like it's worth its own complexity.

Wdestroier commented 1 day ago

An idea is to have if! (expression) element as an alternative to if (!(expression)) element.

mateusfccp commented 22 hours ago

An idea is to have if! (expression) element as an alternative to if (!(expression)) element.

This would be basically an unless (❤️), but I think the main gist of this issue is to have a semantic that makes sense to format in the same column instead of nesting it with indentation.

By having an unless-like condition, by itself, we would still have indented nesting:

if! (a)
  if! (b)
    for (...)
      <expr>

Instead of the proposed

if (!a) continue
if (!b) continue
for (...)
  <expr>