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

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.

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?

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

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

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

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

While this is an interesting solution, AFAICT it doesn't allow an arbitrary placement of break within the loop body, which is unfortunate.

Here's the best I could come up with, still too ugly for real use:

@inline( __always )func pyWhile( body:()->Bool, welse:()->() = {})
{
      if !body()
      {
         welse()
      }
}

var x = 0
pyWhile{
   while x < 5
   {
      print( x )
      if( x >= 3 ) {return true}
      x += 1
   }
   return false
}
welse:
{
   print( "Normal exit" )
}

Note this code has to use "return" to break out of the body.

Output without return:

0
1
2
3
4
5
Normal exit

output with return:

0
1
2
3

I wonder if it would be possible to use Swift macros to implement the equivalent of a Python while...else statement? (Haven't had the time to dig deeply into Swift's macro capabilities, so I don't know).

After giving this subject way more thought that it really deserves, I think I've come up with the cleanest solution I can:

var x = 0
outahere:do
{
   while x < 5
   {
      print( x )
      //if( x >= 3 ) {break outahere}
      x += 1
   }
   print( "Normal exit" )
}

The only bit of syntactical ugliness is being forced to use a labeled break statement (well, plus an enclosing do statement).

The really nice thing about this approach is that it works for anything, not just while loops:

outahere:do
{
   for x in 0..<5
   {
      print( x )
      if( x >= 3 ) {break outahere}
   }
   print( "Normal exit" )
}

In fact, this scheme also solves a problem I've been complaining about elsewhere: you can use it to force a deinit call when the main program of a console application terminates (by wrapping all the outer-level code with a do{...}):

do
{

   class withDeinit
   {
      init()
      {
         print( "Creating class" )
      }
      
      deinit
      {
         print( "deinitializing class" )
      }
   }
   
   let classObject = withDeinit()
   

} // end do

Output from this last snippet is:

Creating class
deinitializing class

The trick is to wrap the entire outer level code with a do{...} statement and deinit() will be invoked when the do{...} statement goes out of scope.

I have to agree with Danny:

do 's power is underly advertised.

1 Like