Stateful loops and sequences

An intriguing request came up on Swift-Ev a day or so ago:

I’ve come across a situation a number of times where I write code that has to try something one or more times using a `repeat…while` loop and the condition relies upon variables that should be declared within the scope of the loop.

repeat {
    let success = doSomething()
} while !success

What caught my eye about this request is the need for state that’s visible both within scope and in the control structure without being declared in the outer scope.

This mechanism already exists in Swift. Meet Swift’s new sequence functions. Swift offers two varieties. Both offer ways to establish state confined to the loop scope.

public func sequence<T>(first: T, next: (T) -> T?) -> UnfoldSequence<T, (T?, Bool)>

public func sequence<T, State>(state: State, next: (inout State) -> T?) -> UnfoldSequence<T, State>

The difference between the two is this: The simpler first function produces a sequence of the same type as its state. The second variation (which actually used to implement the first one) differentiates the state type from the output type, so you could generate integers and operate on strings if you wanted to.

If you think about it, a repeat-while loop really is just a sequence written in a different form. Here’s a simple example that counts by 5.

var i = 0
repeat {
    print(i) // some loop body
    i = i + 5
} while i <= 100

You can rewrite this to incorporate the i-variable state into the control structure like so

for i in sequence(first: 0, next: { $0 + 5 }) {
    print (i) // some loop body
    if i >= 100 { break } 
}

Or you can be slightly more daring and move all behavior and state into the next closure:

for _ in sequence(first: 0, next: {
    print($0) // some loop body
    let value = $0 + 5
    return value <= 100 ? value : nil
}) {}

There are three big things to note about this modification:

  1. The for-loop doesn’t need a variable. It’s only being used to run the sequence.
  2. The loop body is empty. It’s just there to complete the syntax for for in. You could just as easily perform Array(sequence) but that would require memory allocation, which is wasteful.
  3. The sequence must terminate by returning nil. This means the closure’s return type is T?, where T is the type of the first argument. This example returns value,  but it could just as easily return any number (like 0 or 1 or 42 or Int()) because that value will never be used in any meaningful way here. It’s just checking for false/nil.

If you’re going to treat Boolean conditions as optionals, it helps to have a quick way to convert a Boolean to an optional equivalent. This is overkill but it gets the job done.

extension Bool { var opt: Bool? { return self ? self : nil } }

Alternatively, you can build a function that operates on Boolean tests so you don’t need to convert Booleans to optionals when building weird for-loop sequences.

The perform function that follows creates a stateful repeat-while loop, which is more or less what I believe the writer was aiming for. It uses a Boolean test, hides the use of sequence(state:next:), and allows a trailing closure for the body of the loop.

func perform<T>(
    with state: T,
    while test: (T) -> Bool,
    repeat body: (inout T) -> Void) -> T {
    var updatedState: T = state
    let boolSequence = sequence(state: state, next: {
        (state: inout T) -> Bool? in
        body(&state)
        updatedState = state
        return test(updatedState) ? true : nil
    })
    for _ in boolSequence {}
    return updatedState
}

// The following example joins the words into a single space-delimited string.
let joinedWords = perform(
    with: ["Lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit"],
    while: { $0.count > 1 })
{
    (state: inout [String]) in
    guard state.count >= 2 else { return }
    let (last, butLast) = (state.removeLast(), state.removeLast())
    let joinedLast = butLast + " " + last
    state.append(joinedLast)
}.first!

debugPrint(joinedWords)

The key bit to notice is that the initial word array isn’t stored anywhere external to the loop but can be operated upon within the loop body. I believe this is exactly what the original writer asked for when he wrote, “code that has to try something one or more times using a `repeat…while` loop and the condition relies upon variables that should be declared within the scope of the loop”

One Comment

  • Another example, in my opinion, where traditional loops should just be used. I just don’t see the appeal of using a higher level function which has to be lengthily reasoned about.

Join the Discussion

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>