Benefits of else clauses on while loops

A nifty language feature I've built into languages I've created in the past is "welse" (while...else). The syntax is basically:

while expr
{
    *while body*
}
welse
{
    *code to execute when expr is false*
}

Note that if the while loop exits for any reason other than the expression being false (such as a break, an exception, or a return), the welse code does not execute.

If a break exits the loop, any special code that might be needed can be placed immediately before the break. However (other than by some kludgery), there is no way to execute special code when the loop terminates normally (that is, when expr is false). welse would fix that problem.

As I've said, I've actually implemented this and it works nicely (and is quite useful on occasion). I think it would be a nice feature in Swift, too.

2 Likes

What benefit does this bring over

while expr { … }
if !expr { … }

?

1 Like

The original code isn't improved much; you can just do

while expr ? true : {
  print("whelse")
  return false
} () { }

or even

while expr ?~ { print("whelse") } { }
infix operator ?~ : TernaryPrecedence
extension Bool {
  static func ?~ (bool: Self, handleFalse: () -> Void) -> Self {
    bool ? true : {
      handleFalse()
      return false
    } ()
  }
}

But nobody wants to have to use a macro just to ensure all of the bindings and conditions stay in sync when things get more complex.

while
  let value = value(),
  value < limit,
  case let value = modify(value),
  let value = optional(value)
{ }

No need for a new keyword, else will do the job nicely.

while expr {
    *while body*
}
else {
    *code to execute when expr is false*
}
3 Likes

A unified one versus two distinct statements. The unified one is more cohesive.

They're also not guaranteed to be the same. They're only the same if the mutation of the condition happens within the loop.

var expr: Bool { .random() }
while expr { }
if !expr { }
3 Likes

If expr is costly or impure, you don't want to run it again. It becomes more compelling with for loops, where you don't even have access to the expression that caused the loop to terminate. Consider this python:

for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            print(n, 'equals', x, '*', n//x)
            break
    else:
        # loop fell through without finding a factor
        print(n, 'is a prime number')

Now you can simulate this in Swift with a labelled continue:

outer: for n in 2..<10 {
  for x in 2..<n {
    if n.isMultiple(of: x) {
      print(n, "equals", x, "*", n/x)
      continue outer
    }
  }
  print(n," is prime")
}

Both are pretty obscure cases tbh.

You can also blend into this consideration of making loops expressions:

for n in 2..<10 {
  let isPrime =
    for x in 2..<n {
      if n.isMultiple(of: x) {
        print(n, "equals", x, "*", n/x)
        break false
      }
    } else {
      print(n," is prime")
      // see some of the longest threads on this forum for more 
      // discussion of whether a then keyword is needed here
      then true 
    }
  print(n, isPrime ? "is" : "is not", "prime")
}

Again, pretty obscure/code golfy.

6 Likes

FYI: The concept of for-else was discussed back in the day on the mailing lists. A key challenge with this construct is how early exits (such as break, throw, or return) affect the execution of the else block.

Note that if the while loop exits for any reason other than the expression being false (such as a break, an exception, or a return), the welse code does not execute.

Your interpretation appears to differ from Python’s semantics. In Python:

The else keyword in a for loop specifies a block of code to be executed when the loop is finished. [...] The else block will NOT be executed if the loop is stopped by a break statement.

Examining different loop constructs, while-else may be useful in some scenarios, and possibly for-else as well. However, does repeat-else make sense? Under your definition, it wouldn’t apply, whereas Python’s interpretation might allow it — if Python had a do-while equivalent.

Could you explain the reasoning behind your approach and why it differs from Python’s?

1 Like

The two bits you quoted match up: the block is executed only when the loop condition fails.

1 Like

I don't think so. Those seem like the same wording to me.
(Copy and past the following here.)

i = 0
while i < 2:
  print(i)
  i += 1
else: print("i is 2 now.")

while i < 2: print("This never occurs.")
else: print("This does though.")

I don't think so. else on while is for executing the same code when the loop runs zero times as when in runs some non-zero times. repeat doesn't have the ability to run zero times, so all of the code that might go in else can just go after the repeat-while.

(Python doesn't even have do/repeat.)

i = 0
while True:
  print(i)
  i += 1
  if i > 2: break
else: print("There's no reason for this clause to even compile.")
1 Like

I wonder how often is this feature going to be needed, to warrant the associated language complexity increase? Is this more often than, say, using the now unsupported "var" parameters?

func foo(var x: Int) {
    ...
}

instead of:

func foo(x: Int) {
    var x = x
    ...
}
2 Likes

Not quite! Let’s take a closer look at the example from the article I linked:

for x in range(6):
  print(x)
else:
  print("Finally finished!")

In this case, as long as there’s no early exit, the else block runs once the loop completes. However, in OP’s example, either the loop body executes or the else block executes — but not both.

This kind of subtleness draws me away from wanting this feature... I found myself more often wanting a solution to, say, this (pseudo code):

var i = 0
`any kind of loop here` (for, while, repeat) {
    i += 1
    ...
    // use `i` somehow.
}
// i is not needed here. But I had to declare it outside!
// so now it is visible here!

In other words.. there are more pressing and/or inconvenient features in the language than the proposed one.

2 Likes

That's do.

do {
  var i = 0
  repeat {
    i += 1
  } while i < 2
}
i // Cannot find 'i' in scope
2 Likes

I think you just misinterpreted their post. (I do kind of agree with tera that this ends up feeling very subtle though; it’s rare enough to encounter this in Python that I never developed an intuition for this.)

1 Like

Likewise it's very easy to simulate the proposed feature by several means. For example:

func bar() {
    func foo() { // local function
        while condition {
            if otherCondition { return } // instead of break
        }
        print("else block")
    }
    foo()
}

Yes, this is an extra nesting level, and some syntactic dance.. but so is do..

The easiest way to do it is to recreate while using do. do's power is underly advertised.

`while`: do {
  if expr {
    // *while body*
    continue `while`
  } else {
    // *code to execute when expr is false*
    break `while`
  }
}
func `while`(
  _ condition: () -> Bool,
  loop: () -> Void,
  break: () -> Bool = { false },
  else: () -> Void = { }
) {
  `while`: do {
    guard condition() else {
      `else` ()
      break `while`
    }
    guard !`break`() else { break `while` }
    loop()
    continue `while`
  }
}
var i = 0

`while` { i < 3 } loop: { i += 1 }
#expect(i == 3)

`while` { true } loop: { i += 1 } `break`: { i == 5 }
#expect(i == 5)

`while` { i < 10 } loop: { i += 1 } `else`: { i = 0 }
#expect(i == 0)
3 Likes

I'll have to use that trick. :slight_smile:

A nice, but probably unintentional, side effect of the fact that repeat/while was originally spelled do/while before it was decided that syntax should be repurposed for its current use is that all of the control flow keywords work inside of a labeled do block.

This means pedagogically, if someone wanted to explain how loops work then he could desugar it into a do block.

// while(condition) loop
`while`: do {
  guard condition else { break `while` }
  // body
  continue `while`
}

// for element in sequence loop
do {
  var iterator = sequence.makeIterator()
  `for`: do {
    guard let element = iterator.next() else { break `for` }
    // body
    continue `for`
  }
}

// repeat while(condition) loop
`repeat`: do {
  // body
  guard condition else { break `repeat` }
  continue `repeat`
}

// repeat loop
`indefinite`: do {
  // body
  continue `indefinite`
}
3 Likes

personally, I always wanted a for-else construct, where the else block executes if and only if the for branch is never executed for any value in the sequence (thus, including empty sequences)

thus:

for x in [Int]() {
  print(x) // this is NOT executed
} else {
  print("not found") // this is executed
}

for x in 1...3 where x > 3 {
  print(x) // this is NOT executed
} else {
  print("not found") // this is executed
}

for x in 1...3 {
  print(x) // this is executed
  if x < 4 {
    continue
  }
} else {
  print("not found") // this is NOT executed
}

for x in 1...3 {
  print(x) // this is executed
  if x == 2 {
    break
  }
} else {
  print("not found") // this is NOT executed
}
5 Likes