Introduce `until let = {…}` and `repeat {…} until let =` as duals of `while let = {…}` and `repeat {…} while`

The swift while loop will check its condition is true and run its body until the check is no longer true. Super helpfully, the condition can include let bindings to optionals that are then available in the while body. An example:

while let thing = things.popLast() { print(thing) }

Swift also has the commands if and guard. In some sense these form a kind of dual pair:

  • if checks a condition and can bind a let within its body which only runs on true.
  • guard checks a condition and binds a let beyond its else body. It's else body only runs on false.

The pitch here is Swift could add until as a similar feeling dual of while, and also repeat { … } until as a dual of repeat { … } while. A particular utility of these would be allowing a let to be bound beyond the body of the while.

An until loop runs until any let bindings are bound to non optionals, and until all other conditions or checks of the until are satisfied.

As a very small motivating example, here's a while loop I've just written that I would like to replace…

while nodes.count > 1 {
    nodes = self.branchGuide
        .chunkedSlices(of: nodes)
        .map(BTree.Node.init)
}

self.root = nodes.first!.summarised

WIth an until loop this could instead read:

until let root = nodes.first, nodes.count == 1 {
    nodes = self.branchGuide
        .chunkedSlices(of: nodes)
        .map(BTree.Node.init)
}

self.root = root.summarised

I'm not sure if this is just a daft idea, or kind of needless, or easily achieved in other better ways – tips here would be gratefully received! But I think I've wanted to be able to do this in about half of the while loops I've written.

Thank you for looking!

5 Likes

guard is more than just the inverse of if. Its main purpose is for early exits.

Plain until loops don't add much value, since it just replaces the ! in a loop's condition with a new keyword. However, the until let example does seem to make some patterns more expressive.

11 Likes

Absolutely; very much the same effect can be achieved with a while.

The particular intent of the Pitch is being able to bind (with let) some final property of the iteration for use in code following the loop. This is the similar to the support provided by guard, uses of which could, but very awkwardly, be replaced by if.

As you say, there certainly are other ways to write this. I'm pitching this because I think it would be rather clearer, and it seems like a nice complementary addition to while. I'm very doubtful it will be accepted – I can't see it being a high priority! But perhaps it might inspire some other enhancement, or I might learn an equivalently fluent way to write it with the benefit of providing a let binding for code after the loop.

1 Like

guard can be easily substituted by if for an early exit:

guard condition else { return }

… can be replaced with:

if !condition { return }

I prefer the guard here, but simply using it for a condition does nothing I know of that if can't replicate.

Imho, the particular benefit of guard is it can bind a let for use by code that follows the guard, such as:

guard let first = collection.first else { return }
first.doSomething()

Or:

guard let self = self else { return }
self.doSomething()

So, similarly, I feel an until loop would be beneficial not because it simply flips the test of a while loop, but because it allows a let to be bound for use after the loop` (and not within it).

4 Likes

:+1: … I've rephrased the title a little to make clear the let business is the main idea here. Thanks!

1 Like

guard is different than using if for an early return in that in the else block of a guard you are forced to exit the enclosing scope (either by returning or throwing), otherwise the code does not compile. This is in fact what allows guard to conditionally bind a variable in the enclosing scope. Using if, you could forget to return or throw somewhere in the if block and execution would continue to the rest of the function body when you didn’t mean for it to. So beyond inverting the Bool check, guard is the safer and more explicit option for expressing an early return based on some condition.

5 Likes

Right! :+1: , and this is also why an unless makes sense too, I think.

  • Like guard (and unlike while and if), until provides binding in to the enclosing scope.
  • And also like guard (again in contrast to if and while), until explicitly doesn't allow binding in to its body.

If until can bind a variable in the enclosing scope, like guard does, that means you can't use break in the loop body. You can still return and continue, but there's no way to pass the until block and execute the statements that follow until the conditions are met and the variable has been bound.

I'm not sure how often binding variables like that is needed, but I like it.

7 Likes

I'd not thought of that implication at all, @michelf. That's really cool – thank you for working that through.

What happens if nodes is empty? Seems like this could result in an infinite loop.

Yes, if nodes.isEmpty, this particular code would result in an infinite loop. The code the example is taken from maintains an invariant nodes.isEmpty == false.

However, I don't think until is any more prone to exhibiting an infinite loop than a while loop would be. An unwanted infinite loop is entirely possible with either construct and the programmer must be careful to avoid them.

4 Likes