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.

2 Likes

what problem is this solving?

1 Like

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.

1 Like

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.

1 Like

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");
}
4 Likes

Yes! labelled block is an uncommon alternative.

2 Likes

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.)

5 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?

1 Like

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.

2 Likes

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
}
4 Likes

So is there a written proposal for this proposed syntax?

And I don’t think else is confusing at all – if it’s really, we can use unbroken.

Unfortunately else with loops can create problems with currently valid code.

Example:

if (true)
for (const v of []) console.log(v)
else console.log('nope')

In this code, with your proposal, else is supposed to be associated with the for loop but it is in fact currently valid code where else is associated with if.

I suggest to use another keyword, empty for example.

if (true)
  for (const v of []) console.log(v)
else console.log('not true')
if (true)
  for (const v of []) console.log(v)
  empty console.log('array is empty')

The flaw in this code is that curly braces are omitted. After gotofail, I would have hoped that everyone had internalized that optional curly braces should always be explicitly present.

It's not always dangerous - for example, if you put the "if" and it's statement on the same line, there's not really any risk:

if (...) ...

Now the chances of someone squishing an if, for, and statement on the same line, then an else on the next line seems unlikely - unless that someone was a minified maybe?