Recommended way to initialise a mandatory property in a loop?

This is a very beginner question.

How do you usually deal with initialising a mandatory (let) property of a struct, in a loop or other iterative process?

I attempted to write something (approximately) of the form

struct Bing {
    let bang: String

    init() {
        while true {
            // do something iterative

            if /* some condition */ {
                bang = "bong"
                break
            }
        }

    }
}

hoping that the compiler would be able to analyse the loop exit conditions and determine that the variable must always be initialised, but apparently not, and I get the expected error: return from initializer without initializing all stored properties.

In a language with loop expressions, I'd tackle this problem that way, yielding / returning the value from the loop: bang = while true { ...

What would you suggest doing in Swift?

No, that would be equivalent to solving the halting problem, while compiler can't do even this:

func foo(_ condition: Bool) -> Int {
    let x: Int
    if condition { x = 42 }
    if !condition { x = 24 } // 🛑 Immutable value 'x' may only be initialized once
    return x // 🛑 Constant 'x' used before being initialized
}

Do you promise to "break" only when "bang" is assigned? And not assign "bang" more than once? Compiler won't be able to check, so it's up to you.

Typically you'd do one of these:

1. var bang: String = ""

and then reassign (exactly once), compiler will not check that you did.

2. var bang: String!

and then reassign (exactly once). compiler will still not check that you did, but you'd at least crash in runtime if you forgot to assign at least once.

3. var bang: String! { // "set-only-once" property
    didSet {
        precondition(oldValue == nil && bang != nil)
    }
}

this will crash at runtime in cases when you reassign the property more than once

    4. 
    bang = f(...)
    func f(....) {
        while true {
            if ... { return "..." }
            ....
        }
    }

i.e. refactor the while loop into a (local) function. This one won't work in "init" though.

This reminds me we haven't finished yet with the multi-statement-if-switch-do-expressions pitch. Loops were not part of that pitch but were brought up during the discussion.

2 Likes

Thanks for the detailed answer! I'd considered 1 and 4 as options, but here I think 2 makes the most sense, as a balance of readability, obviousness, and safety (and I didn't know about that syntax).

Notably, 1 doesn't seem to work well for types without a simple "default" instance (something more complicated than String).

Yes, I'm fully aware that compilers can't in general reason about loop exits, but LLVM is pretty good at figuring out simple ones later on, during optimisation, and when warning about dead code. I wonder how infeasible it'd be to do some of that transformation early enough that semantic analysis like this could benefit from it?

Anyway, that's no criticism of Swift, most languages have the exact same issue with code like that.

Neat!

I'll confess, compared to a language designed around everything being an expression to begin with (say, Rust, or a fully functional language like Haskell), the proposed syntaxes seem... really clunky.

But maybe still worth doing, including for loops - for cases just like this. Formalising the idea of a loop "producing" a value lets the compiler check all the things you want it to check.

while true is already special cased to be known as an infinite loop for unreachable code analysis, so I'd call it a bug that we don't handle it as such for definite initialization analysis.

4 Likes

After trying a few of these, I settled on putting the loop inside a closure and assigning bang to the output - i.e.

struct Bing {
    let bang: String

    init() {
        bang = {
            while true {
                // do something iterative

                if /* some condition */ {
                    return "bong"
                }
            }
        }()
    }
}

I'll have a look later to see if and why a normal local function wouldn't work, because a closure seems to!

2 Likes

You can use statement labels to work around this:

struct Bing {
    let bang: String

    init() {
        loop: do {
            // do something iterative

            if /* some condition */ {
                bang = "bong"
                break loop
            }
            continue loop
        }

    }
}
2 Likes

Ooh, I like this!

Or even inverted:

guard /* some condition */ else {
    continue loop
}

bang = "bong"

makes it very clear what's going on.

2 Likes

I find having a possibly infinite loop in init() feels too hacky. So how about moving it outside init()? This version not only compiles without the tricks in above posts but also is easier to understand.

struct Bing {
    let bang: String

    init(bang: String) {
        self.bang = bang
    }

    static func create() -> Self {
        while true {
            if Bool.random() == true {
                return Bing(bang: "hello, word")
            }
        }
    }
}

P.S. If I understand Joe's comments correctly, it's a bug that your original version didn't work. My version and your closure version avoid that bug.

Fair enough. This was a minimal, simplified example; in the actual code I was working on, the loop is pretty clearly terminating. And if you're implementing certain protocols (e.g. Decodable), you don't have any choice but to put things in init.

Closures and local functions should work so long as they don't use (capture) self. Instance member functions (methods) won't work as they are considered capturing self implicitly. If you are not using self one way or another – you may as well use a static method in your init.

A quick question: why does capturing self make a difference?

Before all stored properties are initialised self can't be used because otherwise you'd be able accessing its not yet initialised properties (either on the spot or by calling something that will do) which would be totally unsafe. Sometimes this rule is overly conservative and deems a bit too strict, e.g.:

class C {
    let x: Int
    init() {
        print(ObjectIdentifier(self)) // 🛑 can't do this yet, even if ObjectIdentifier won't need "x"
        x = 1
        print(ObjectIdentifier(self)) // ✅ ok
    }
}

but in the vast majority of cases this rule makes total sense.

Yes, it's a bug.
Should be fixed here: Mandatory optimizations: constant fold boolean literals before the DefiniteInitialization pass by eeckstein · Pull Request #70787 · apple/swift · GitHub

4 Likes

I wouldn’t recommend using an implicitly unwrapped optional in this case, in fact I tend to avoid them in general because of the reduction in safety.

I also think using while true probably isn’t the best way to do this. You want to loop until some condition is met, so I would use that condition in the while loop.

// initialize any loop variables
while ! /* some condition */ {
    // the iterative stuff
}
self.bang = /* some value */

Of course this code is slightly different than yours since the condition is also checked before the first iteration happens, but it can probably be adjusted to the specifics of the situation.

Implicitly unwrapped optionals are not "unsafe". It is "safer" to have the app trap (and terminate) because you forgot to initialise the variable properly, than to continue with some default value in the not-properly-initialised variable as if nothing bad happened.

I'd like to see what do they use in Rust/Haskell/etc in this situation.

You’re right, I meant it in the sense of safe from my future self introducing a bug. Losing the guaranteed initialization and the assurance that it can’t be directly assigned a new value makes it less safe in that sense because it doesn’t have compiler guarantees about “proper” usage.

1 Like

For a very specific and limited definition of 'safe'.

I think @johnny1 is wise to be hesitant to use implicitly unwrapped optionals, because "but it's fine, the app just crashed" is absolutely not a consoling response to any of the app's users. :slightly_smiling_face:

He did say reduction in safety, which I think is an accurate characterisation in lay terms. Not having the ability to crash - because the compiler ensures the pointer cannot ever be invalid - is definitely safer, even by Swift's overload of the word.

3 Likes

Which I believe is not what happens with IUO's. The idea is that the app crashing will be picked up and fixed early during the app lifecycle, while the bug of not properly initialised variable could get unnoticed for a considerable amount of time. That's why our array's access could crash the app on out of bounds errors, when "messaging nil" we are not getting 0 magically, and so on and so forth. Would you prefer a banking app that transfers an incorrect amount instead of crashing?

Your right that they're technically safe, since they crash if you try and read them before initializing, but Implicitly Unwrapped Optionals are never needed in todays Swift. Definite initalization, lazy vars, and pooperty wrappers cover every concievable use of them. IUOs were only added to support @IBOutlets before we got property wrappers. They are legacy cruft only. Please do not use them in new code.

As to the main topic, the original code works fine once you use the other kind of while loop:

struct Bing {
    let bang: String
    init() {
        repeat {
             // the iterative stuff
        } while ! /* some condition */
        bang = "bong"
    }
}
2 Likes

I do not see this kind of language in the Swift book (and if what you said was true I'd expect to see a relevant wording in there). This is what it has to say about IUO's:

See the full Country / Capital example in there. If Country and Capital are classes, every country must have a capital and every capital belongs to the relevant country and you don't want the unnecessary unwrap noise in the rest of the code there's no other option than using IUO's.


As far as original question goes I believe the best workaround would be to use a static method:

struct Bing {
    let bang: String

    init(param: Int) {
        bang = Self.staticMethod(param: param)
    }
    
    static func staticMethod(param: Int) -> String {
        while true {
            if /* some condtion */ {
                return someValue
            }
            ...
        }
    }
}