Proposal - `for .. else ...` (also `while ... else ...`) statement

for now:

let found = false
for (/* condition */) {
  if (/* conditional */) {
    found = true
    break
  }
}
if (!found) {
  // ...
}

proposal:

for (/* condition */) {
  if (/* conditional */) break
} /* without break */ else {

}

As mentioned above, if the loop is not terminated prematurely with the break statement, the else block will be executed.

1 Like

what problem is this solving?

As mentioned in the first code block, sometimes the loop will be terminated earlier after meeting the intention, but not in the other time, the loop is just a try, after which we need to do more tries. the for...else statement will make the intention express more concise.

Of course, a heavy logic of the loop statement separated to a function is better sometimes. :sweat_smile:. But the for...else... is still a good expression in much of time.

You could use a labelled block. Though your original code with the found variable is maybe still cleaner.

search: {
  for (const x of [1,2,3]){
    if (x === 9) break search;
  }
  console.log("not found");
}
2 Likes

Yes! labelled block is an uncommon alternative.

1 Like

The main issue with the for/else construct, is that the naming is confusing.

(Also, the pattern of break targeting a labelled block deserves to be better known, but this is an educational issue.)

4 Likes

I'm struggling to find a scenario where Array#find wouldn't suffice.

const maybeItem = [... iterable].find(
  i => /* conditional with iterable element i */
);

if (!maybeItem) {
  // else stuff here
}

Is this less readable or understandable than a for..else loop?

Yes, and it always exhausts the whole iterable, meaning it cannot be used with infinite iterables, or when you actually need to stop taking elements after the stop-condition.

Interesting idea. I think the else block is doing the opposite of what it's saying it's doing. So I propose to have then instead.

for (const { id } of products) {
  if (id == 8) break
} then {
  console.log("404: Not Found!")
}

The then block is supposed to get executed if the loop finishes without breaking.

Makes more sense in my opinion :grinning_face_with_smiling_eyes:.

I don't understand this confusion, but apparently it's common as some people struggle with it in Python as well. Here's how you should read it:

if (COND) THEN_BLOCK
else ELSE_BLOCK
// so far so good? ELSE_BLOCK runs when COND fails

while (COND) THEN_BLOCK
else ELSE_BLOCK
// this works exactly like the above, ELSE_BLOCK runs when COND fails,
// except here the COND and THEN_BLOCK may be evaluated more than once

for (INIT; COND; NEXT) THEN_BLOCK
else ELSE_BLOCK
// ELSE_BLOCK runs when COND fails

for (value of ITERABLE) THEN_BLOCK
else ELSE_BLOCK
// here you need to perform some mental gymnastics
// to figure out what the condition is;
// translates rougly to:
let value, done, iter = ITERABLE[Symbol.iterator];
while ({ value, done } = iter.next(), !done) THEN_BLOCK
else ELSE_BLOCK
// ELSE_BLOCK runs when the condition !done fails,
// i.e. when the iterable is exhausted

That being said, JS doesn't need else on loops, break from labelled block fulfils this role.

1 Like

Though the iterator helpers proposal should cover that use case, correct?

From their README

function* naturals() {
  let i = 0;
  while (true) {
    yield i;
    i += 1;
  }
}

naturals().find(v => v > 1); // 2

I do want to note that this has language precedent: Python. And they apply it to a variety of constructs.

It does have its uses (as exemplified in this SO answer), but it does also have a fair bit of FUD (ranging from confusing naming to the frequent presence of alternatives).

I'm with others in that a labelled block is better, and IMHO that idiom is a bit underused anyways (it's a structured goto, and do { ... } while (false) essentially just emulates a labelled block in a more boilerplatey and less immediately clear fashion).

1 Like

Yes, labelled block should be more well-known. it's very hard to see it has been used.

I want to add that template engines (like Twig) also provide this construct and there it works very differently: There the else gets executed if no iterations happened because there were no items to loop over. So, IMO, if for ... else gets implemented, it should work like that. The other question is: is that necessary or any helpful? It would just replace if(foo.length === 0) { } with else { }, right?

If such a statement comes to exist, this IMHO is the right way to do it. Way easier to break out initially than later, and I find myself using one of these two patterns (with both for and while) way more than nested blocks or similar:

// 1. Check length first
if (list.length) {
  for (let item of list) {
    // enumerate item
  }
} else {
  // list empty
}

// 2. Check if loop body executed
let hasItems = false
for (let item of list) {
  hasItems = true
  // enumerate item
}
if (!hasItems) {
  // list empty
}

Occasionally, if I'm coding a perf-sensitive bit and am working with iterables, I'll write out the desired loop manually:

let iter = set.values()
let result = iter.next()

if (result.done) {
  // set empty
} else {
  do {
    let item = result.value
    // enumerate item
    result = iter.next()
  } while (!result.done)
}

But in any case, it'd be super nice to just write it as this:

for (let item of list) {
  // enumerate item
} else {
  // list empty
}
2 Likes