Pre-pitch: remove the implicit initialization of Optional variables

Better question, how many people even know that Optional<T> avoids implicit initialization in the first place?

10 Likes

I think that 'very inconsistent' is a strong descriptor here, especially when considering the history of Swift implementing a lot of 'features' (implicit returns, trailing closures which explicitly contradict the declaration of a function, and the entirety of control flow in ResultBuilders) for the sake of having 'light weight syntax'.

Implicitly assigning nil to an optional is an example of 'light weight syntax' which no real drawbacks.

If implied = nil was not a current feature, I imagine that we'd have a pitch to "Allow the programmer to elide = nil on optional declarations". The proposal would note that this would create 'light weight syntax', and that there would be no effect on ABI. And most of us would cheer.

3 Likes

And the Review Team would most likely reject it because it'd make ? syntax even more magic than it already is, in addition to introducing a rule that doesn't apply to any other type... including Optional<T> itself. The argument can be made both ways.

4 Likes

I do understand the idea, however, I don't agree with this.

? and ! are special sigil and syntactic sugar, whose are not equal to normal variable declarations.
Personally I guess what is inconsistent here is that you can declare it in both ways, explicitly and implicitly.
Those are all valid:

var valueA: Int?
var valueB: Int? = nil
var valueC: Int? = 0

To me valueB feels redundant / noisy and should be declared like valueA.
Because valueB has default assignment while actually it has no value.

So... this will be opposite of what this pre-pitch is proposing, however, why not make valueB compile-error (or warning)?
Just an idea.


However, that said, I'm too familiar with Swift across its older to newer versions.

T? is usually thought of as syntactic sugar over Optional<T>, the same way [T] is sugar over Array<T> and [T: U] is sugar over Dictionary<T, U>. Through that perspective, default initialization makes no sense — var arr: [Int] doesn’t initialize arr with an empty array. Default initialization for values of T? is additional sugar over T? = nil.

In the code sample you showed, the code for valueA is misleading: it reads like an uninitialized variable, but it is actually initialized as Optional<Int>.none. Declaring instances of T? and T! is the only place in Swift that has a concept of default initialization (aside from property wrappers) — there’s no concept of default values for integers and empty collections, even though there is in other languages like Java. In addition, valueB doesn’t have “no value” — its value is Optional<Int>.none (a.k.a. nil), just like valueA. While valueB’s initialization is noisier, its meaning is clearer and it’s still not that noisy.

Swift has also never had a warning or error for being “too verbose”. If anything, Swift’s errors often make code more verbose in order to clarify that the programmer is doing something intentionally.

// these things have potentially unexpected behavior so Swift makes us confirm that this behavior is intentional by making it more verbose
let thing0: Void = thing1 = 5
NSSomeClass.doThingInObjC(nil as String? as Any)
let string: String? = (instanceOfAny as! String)

Swift allows the programmer to make code more verbose than is necessary so that they can make their code more clear. For example:

if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowsOverridePassword { … }

is more readable than

if enteredDoorCode && passedRetinaScan || hasDoorKey || knowsOverridePassword { … }

, despite the unnecessary parentheses.

That being said, I would welcome a prettier way to explicitly express nil optionals. var number: Int? = nil is ugly, and var number = Int?.none, while valid, is weird. Perhaps Optional should have an initializer with no parameters that is equivalent to .none.

var number = Int?()

This would match how empty RangeReplaceableCollections can be initialized (var arr = [Int]()) as well as how zero for BinaryIntegers / Double / Float can be initialized (var num = Int()).

4 Likes

On the contrary, I think a cornerstone of the argument to remove this “feature” is that we most certainly would not propose to add it if we didn’t have it:

There’s a pretty strict precedent established that Swift doesn’t default initialize values. Moreover, as noted by others, it is starkly inconsistent that var x: T is an uninitialized variable except when T is explicitly spelled out as an Optional type using the postfix operator. Finally, it is problematic that (as per @jrose’s linked tweets) there are performance consequences to having it work this way.

Can you imagine a proposal going to review for syntactic sugar that blurs the distinction between initialized and uninitialized variables and adds performance gotchas?

9 Likes

these performance gotchas will still happen in explicitly initialised cases (until addressed), no?

var x: Int? = nil
var y: Optional<Int> = nil

I'm not sure I entirely believe that this is the position you want to stake out. What inconsistency are you referring to here?

If you're suggesting that it's inconsistent for some properties to have no initializer expression in their declaration, that ship's already sailed, and it ain't coming home:

struct S {
    var x: Int // <-- no initializer!!!
    init() {
        x = 1
    }
}

Of course, I know you don't mean that. You mean that the Rule of Consistency says that a type with var x: Int must initialize x with an expression somewhere in the code. That ship has sailed too, though, because synthesized member-wise initializers are a thing:

struct S {
    var x: Int // <-- OMG, inconsistent!!!
}

Of course, that's not the Rule either. The actual Rule is that properties must be initialized somewhere — even if the compiler does it for you, and even if you can't see it by inspection of the source code, and even if you may have to reason about the code in order to see where the initialization value comes from. Implicit initialization of optionals doesn't violate this rule.

I happen to think that the harm to developers familiar with Swift — in imposing a requirement for boilerplate = nil forever and everywhere — is a significantly greater burden than (say) expecting newcomers to have a single moment of realization that an implicit initialization is the default for optionals.

2 Likes

But, as you note, this wouldn't be the outcome. It would be perfectly acceptable to omit the initial value and initialize the optional property out-of-line in an init, or to allow the memberwise initializer to be synthesized as appropriate.

Good. So you understand exactly what I mean. Appreciate your clarifying for all.

1 Like

I don't know what the point was in responding to thing nobody has claimed, but this part is easy to answer:

Why would optionals be different from other types? No other type is implicitly initialised. Is it "boilerplate" to have to write var numberOfRows = 0 too?

2 Likes

It's hard for me to express since I lack the precise terminology required, but optional nil values are unique in that they apply equally to all optionals, everywhere. That is, they have the same meaning for all instances: .none. The same is not true of 0 or [] or [:]. Each instance of those types may mean something different for the empty value, or the empty value may be invalid altogether. In that way nil is a valid default value where the other instances aren't.

Also, nil defaults are likely an order or even orders of magnitude more common than other defaulted values, especially when interacting with Obj-C APIs. In that way the burden of having to explicitly initialize them is far greater than Ints or Arrays. So even if you don't consider the type-based default value argument to be valid, it's still a matter of the sheer scope of the change you're talking about here.

2 Likes

I understand what you mean, but I don't think it's true. in let x: String? = nil the nil does not mean the same thing as in let x: Int? = nil.

Optional<String>.none is not the same as Optional<Int>.none, in pretty much exactly the same way an empty String array is not the same as an empty Int array.

If you are talking more conceptually then sure, different nils have sort of the same meaning, but that is also true of different empty arrays.

1 Like

Exactly. nil being a magical placeholder for Optional<T>.none is convenient but also obscure. And hiding it altogether as an implicit default is even more damaging to the mental model. Hence this thread.

Not really, no. Just because Swift's type system requires explicitly typing optional values doesn't mean Optional<Int>.none and Optional<String>.none can't be considered the same things. In Haskell I believe they'd both just be the Nothing type instead. Whereas empty arrays have a variety of different internal behavior depending on the type your using.

Obscure? How so? While people have pointed out the edges to Swift's implicit nil model in this thread, I haven't heard any actual harm or even damage that is unique to this feature vs. other implicit Swift behaviors (where they can be surprising or implicitly change behavior). The model seems quite clear to me; declarations using the ? form will default to nil when used as a property or local variable. The other behaviors seem to fall clearly out of those two behaviors. This mostly seems to be a documentation or edge definition issue.

1 Like

This thread exposes a few of the damages. Here's another one:

Those don't seem to be damages, unless you consider every misunderstanding about language behavior damaging. In which case they come and go as the user gets more familiar with the language.

Edit: And I guess my ultimate point is that those edge cases can be fixed without removing the feature from the language.

I mean yeah, you can "consider" anything to be anything, but in the language, they are different instances of different types. And if you want to talk conceptually, it's hard to argue that nil is fundamentally different from [], that is just subjective.

What differences would that be?

By the way, even if optionals would be different from arrays etc, I don't think that's an argument. They are still a type, with instances and values, it can be var or let, you can extend it etc etc. So it should still behave the same w.r.t. implicit initialisation as other types.

I've been working with Swift professionally for 5+ years. I'm very familiar with it, and I mistook this behavior for a compiler bug just last year. It is a problem beyond unfamiliarity.

8 Likes

How do you distinguish between simple unfamiliarity and a broken feature then? I've been working in Swift quite a while as well, yet I forget the edges of the generics system, initializer inheritance, or other rules fairly regularly. That doesn't mean the features are broken. It may mean the compiler could be improved, documentation could be added, or that I'm tired at some particular moment. So I think we need more justification for language changes than "sometimes people forget this rule".