Pitch: Promote optional property to non-optional in local scope

A common pattern in UIKit is to delay initialization of subviews until viewDidLoad. The compiler should be able to infer in the below sample code that in the viewDidLoad scope testSubview is a non-optional:

class TestViewController: UIViewController {

    private var testSubview: UIView?

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        testSubview = UIView()
        
        /** Value of optional type 'UIView?' not unwrapped; did you mean to use '!' or '?'? */
        view.addSubview(testSubview)  
    }
}

This is easily solvable by doing the following:

    let testSubview = UIView()
    view.addSubview(testSubview)
    self.testSubview = testSubview

But an intelligent compiler shouldn't require this workaround. This type of inference would make working with optionals even easier in Swift, and solve a common and unnecessary pattern.

1 Like

Is that true?

If another thread sets testSubview to nil after testSubview = UIView() is executed, but before view.addSubview(testSubview()) is executed, then what?

2 Likes

Concurrency issues aside, this is known as flow-sensitive typing and has been implemented in a few languages.

As for your specific example, an even easier solution is view.addSubview(testSubview!), as suggested in the error message.

IMO if you run into problems like this, the API you are using (in this case, UIKit), instead of Swift, is the one that need to be improved.

For this specific example, ideally testSubview should be non-optional and your code should look like this:

func init() {
    testSubview = UIView()
    view.addSubview(testSubview)
}
1 Like

Indeed, flow-sensitive typing has been discussed on this list in the past. However, in this case, I don't think that testSubview can be inferred to be non-optional even if the compiler supports flow-sensitive typing. It's not a local variable and, although private, viewDidLoad() or another method with access to it could be called in another thread.

It is an unfortunate consequence of UIKit patterns but there’s not really an automatic compiler solution here, unless that property can somehow be marked to be not manipulated across multiple threads.

One way to avoid the optional unwrapping is to initialise the value with an executing closure:

override func viewDidLoad() {
    super.viewDidLoad()

    testSubview = {
          let testSubview = UILabel()
          testSubview.text = "Text"
          testSubview.font = UIFont.systemFont(ofSize: 12.0)
          view.addSubview(testSubview)

          return testSubview
    }()
}

You don’t save characters in the case where you only initialise a view and instantly add it, but if you are setting other properties, you avoid the repeated optional unwrapping by setting them on the non-optional local variable inside the closure.

Is this better than simply declaring a local variable with another name and then assigning to the property at the end? Much of a muchness, but it’s often cleaner to encapsulate all configuration inside the closure.

One other thing you could do is declare the property as var testSubview: UIView! in the first place, if you know it will always be non-nil when you eventually use it, as your code logic does not depend on it until after viewDidLoad() is complete. I don’t like this as much, but it’s not the enemy like some tutorials and blogs like to portray. ImplicitlyUnwrappedOptional exists for a reason (tangential: the IUO type is on the verge of deprecation and going away soon, but the concept will remain — spelled as T!).

3 Likes

Are accesses and mutations of class properties implicitly atomic, then? In my understanding of C++ and Rust, these kinds of race conditions are always undefined behavior unless you use an atomic type; otherwise the compiler wouldn't be able to cache repeatedly-accessed properties in registers. I conservatively assumed that this would be the case in Swift as well.


I would definitely like to see a feature like this. Perhaps this could even be generalized to flow-sensitive pattern exhaustiveness checking. Optional is an enum (albeit a very special one), and so if the compiler could (through some guaranteed SIL pass) make reliable proofs about which enum cases where actually possible for different variables, it could both solve this problem and reduce the number of impossible defaults developers have to put into switch statements.

I’m yet to be convinced creating a Compiler Feature which assumes that a property won’t be re-set on another thread, or isn’t set as a side effect of other methods in your flow (imagine if I called an updateViewDetails method that reset that property to nil), is a good idea in the slightest. This is asking for a deliberately unsafe convenience which violates the fundamental reason optional exists in the language - to ensure you handle the case in some fashion.

If you are sure that a property is non-null, you can mark the property as implicitly unwrapped optional, or force unwrap it. This marks the property as you are acknowledging the risk and still wish to unwrap it or fatalError.

Personally I prefer the other option, a shadow reference, which was shown earlier. This is slightly safer because you can be sure at compile time that all configuration you do on the one instance and don’t accidentally configure things if the property changes at some time due to some side effects.

From memory, Swift variables are at this stage non-atomic. This would mean, technically, anything being switched from multiple threads ends up in undefined behaviour if called when accessed at the same time as a set. That however is exceedingly rare except for high churn code, and we need to plan when this is looking to be addressed in I believe Swift 6 with Swift’s concurrency story.

That said, promoting optionals under the premise “it’s technically a risk anyway” is short sighted and doesn’t account for other cases where the variable might be set. Even if it is a private variable, there can be effects on that property eg when calling other methods, which the compiler would have to look ahead and play “God Mode” to ensure it never re-sets back to nil, that it is not a dynamic property that could be adjusted by the runtime, that it is not weak... aren’t we going too far just to add a tiny convenience, with a tonne of compiler complexity?

And then we have to explain to new devs why at some times the compiler assumes it’s non-nil and at other times, mysteriously, it cannot assume it... or the assumption changes after a seemingly unrelated edit a later stage and breaks unrelated code?

All very good points. I don't think the fragility issues are entirely unsolvable, but we are talking a massively complex feature (implementation wise) for questionable benefit.

That said, I would agree that flow-sensitive typing in situations where it can be entirely safe would be a nice-to-have at some point down the line.