Can anyone explain this behavior in Optional Chaining?

Documentation states the following

Specifically, the result of an optional chaining call is of the same type as the expected return value, but wrapped in an optional. A property that normally returns an Int will return an Int? when accessed through optional chaining.

Consider the following code (output as inline comments)

class Person {
    var nonOptString: String = "non opt"
    var optString: String? = "opt"
}

var person: Person? = Person()
print(type(of: person?.nonOptString))  // Optional<String>
print(type(of: person?.optString))  // Optional<String>

The output of the first type(of:) can be explained with this part of the docs

A property that normally returns an Int will return an `Int?

String instead of Int and String? instead of Int?, makes sense.

However, I do not understand the the output of the second type(of:). The output I get is String?, going with the above statement, shouldn't I get String?? (String? wrapped in an optional)

Under Linking Multiple Levels of Chaining:

You can link together multiple levels of optional chaining to drill down to properties, methods, and subscripts deeper within a model. However, multiple levels of optional chaining don’t add more levels of optionality to the returned value.

7 Likes

In early versions of Swift you did in fact get nested Optionals, but it was widely agreed to just be annoying. There's rarely any semantic clarity or benefit otherwise in having multiple layers of optionality.

Though if you do encounter one of those rare use-cases, you can mimic it with intermediary wrappers, e.g.:

struct Wrapper {
    var value: String?
}

func gimme() -> Wrapper? { … }

let value = gimme() // Value is essentially String??, just it has to be
                    // used as e.g. value?.value?.count

(in a real-world situation you'd likely use more meaningful names, in context, which helps prevent confusion)

1 Like

Really? I don't recall optional chaining ever working differently than it does now. Do you or anybody else have a link that shows when that change happened (if ever)?

There's the fairly recent SE-0230, but that's only about flattening double optionals caused by try?.

But the example has only one level. No?

I guess that's what I was thinking of. I thought there was more to it than that, but to my surprise nested optionals are still supported in Swift 5.9.2, same as they were going back years. Curiously, I haven't encountered nested optionals in real code in years, so I guess try? was their only real source.

1 Like

Incidentally, the very first paragraph of SE-230:

Swift's try? statement currently makes it easy to introduce a nested optional. Nested optionals are difficult for users to reason about, and Swift tries to avoid producing them in other common cases.

It doesn't try very hard. e.g.:

var d = Dict[Int, Int?]()

let whoops = d[0]

Although note that Dictionary doesn't really support Optional value types anyway, because you can't insert a value of nil (any attempt to do so only removes the key-value pair entirely, since Dictionary special-cases the meaning of nil during insertion).

d[1] = .some(.none)

print(d.count)
// 1
2 Likes

This ergonomic issue is part of the "difficult[y] to reason about" mentioned in SE-0230, but the avoidance of nested optionals doesn't extend to changing the API of types that traffic in Optional explicitly. You can use optionals in a dictionary, but you do need to take care to clarify whether you're passing around Optional<Optional<Value>> or Optional<Value>, since nil makes the actual type invisible.

2 Likes

I wouldn't say that Dictionary does this because it isn't special in this regard. The same rules apply to any variable of double-Optional type. Assigning nil to such a variable results in .none (i.e. the outer Optional is "empty"). If you want to assign .none to the inner Optional, you have to write .some(.none):

let a: Int?? = nil
let b: Int?? = .none
assert(a == b)

let c: Int?? = .some(.none) // or .some(nil), or Optional(.none)
assert(a != c)

The same works for dictionaries:

var d: [Int: Int?] = [:]
d[1] = nil
assert(d.count == 0)
d[1] = .some(.none)
assert(d.count == 1)
5 Likes

The next few sentences further clarify :smiley:

To put it another way:

  • If the type you are trying to retrieve isn’t optional, it will become optional because of the optional chaining.
  • If the type you are trying to retrieve is already optional, it will not become more optional because of the chaining.

Therefore:

  • If you try to retrieve an Int value through optional chaining, an Int? is always returned, no matter how many levels of chaining are used.
  • Similarly, if you try to retrieve an Int? value through optional chaining, an Int? is always returned, no matter how many levels of chaining are used.
5 Likes

I’ve had to use them once recently, where a value may or may not be present, and when it is present its type is still String?: Dependiject/Dependiject/MultitypeService.swift at d6a2cac4bd301d6e880e368147bf4284115b1e8d · Tiny-Home-Consulting/Dependiject · GitHub