Hi,
I would like to simplify the reduce some boilerplate of nested Optional.
Motivation
Optional provide us with a simple mental model that "There may be some value here, but maybe not for whatever reason", and is useful for checking if operation succeed and reading the overarching results.
The nested Optional removes that mental model since the it then means different things in different layer of nil and programmers may need to keep track of them.
Having more information (in this case, the cause of nil) can be useful, but the problem with nested optional is that it provides information without being expressive.
For example,
let a: [T?] = [...]
guard let firstTmp = a.first else {
// statement 1
}
guard let first = firstTmp else {
// statement 2
}
This takes some investigation to see that statement 1 handles case where a is empty, while statement 2 handles the case where a is not empty, but first element is nil.
If statement 1 and statement 2 are the same, one can do:
guard let firstTmp = a.first,
let first = firstTmp else {
// statement 1/2
}
Or
guard let first = a.first as? T else {
// statement 1/2
}
The first one still introduce variable firstTmp, while the second one is more of a casting magic.
Nested Optional may also lead to surprising behaviour:
let optional1: T?? = nil
let optional1: T?? = .some(nil)
print(optional1 != nil, optional2 != nil)
// false true
optional2 returns true despise not having any T value. This may not be what programmer expect.
Note that most of the time, the nested Optional isn't directly created, but is a result of generic functions/classes having Optional as one of its associated type such as a: [T?] above.
Solution 1: Recursively Unwrap Optional on Explicit Unwrapping/Optional Chaining
if-let, guard-let, optional chaining (?.) and unwrapping(!) will statically try to unwrap value as much as possible by default, or if resulting type is specified, until it reaches the specified type e.g.
let maybeNil: T??? = ...
maybeNil?.foo() // foo is a function of T
if let stillOptional: T? = mayBeNil {
// stillOptional is of type T?
}
if let notOptional = maybeNil {
// notOptional is of type T, even if T = U? at runtime
}
It'll result in similar behaviour on nested vs unnested Optional at use site, and allow for previous behavious using migrator.
This solution still needs to handle checking nil-ability (see optional3 above) since it permits the nested Optional. It may introduce a new flattening syntax ?
let maybeNil: T??? = ...
let flattenedOptional = maybeNil?
// flattenedOptional is of type `T?`
And so checking for nil-ability can be done as follows
if maybeNil? == nil {
// Do something
}
Solution 2: Flatten all Nested Optionals
This solution will flatten all nested Optionals into simple Optional. This solution may cause problem with Generics that return or take in optional. It is possible to do, but must be threaded carefully and can be risky even so.
To allow flattening, one may add an associated Any value to nil so the following would compile
let a: T? = nil(anyData) // anyData is `Any` type
This may cause overlapping of duty with throwable, and I think try/throws would do better job in this regard.
Solution 3: Maintain the Status Quo
Nested Optional may not need any modification, though I don't think something that "provide information without being expressive" is very swifty.