Double-optional nil comparisons seem non-intuitive

This result in this example makes perfect sense:

var shouldReturnNil: Bool? = nil
let doubleOptionalValue: String?? = shouldReturnNil.map { $0 ? nil : "MyValue" }
doubleOptionalValue                 // nil
doubleOptionalValue == nil          // true  πŸ‘

But when the value of doubleOptionalValue is instead a nested nil, comparing it to nil- which seems completely intuitive- returns false:

var shouldReturnNil: Bool? = true
let doubleOptionalValue: String?? = shouldReturnNil.map { $0 ? nil : "MyValue" }
doubleOptionalValue                 // Optional(nil)
doubleOptionalValue == nil          // false  🀯 🀯 🀯

I do understand why this is happening; the nil expression in the last line is expanded to Optional<Optional>.none, which isn't what doubleOptionalValue equals at that point. This is really what I was looking for:

doubleOptionalValue == Optional<Optional<String>>(Optional<String>(nil))     // true

But in reality, all we really care about 95% of the time is whether the base value is nil, not an intermediary wrapper level. That was the intention behind flattening optional try? statements, and while that's not directly related to this, it feels like it's in the same vein. (And if I'm understanding his intentions correctly, it might also be counter to doing "something sensible" as Jordan Rose said in Optional safe subscripting for arrays - #23 by jrose).

I'd love to hear thoughts on this- please let me know if I'm misunderstanding how these comparisons are supposed to work!

(All issues found using Swift 5.0, Xcode 10.2.1)

2 Likes

You are not wrong comparing the situation with the try? flattening. Double optionals are uneasy to deal with.

You may prefer using flatMap instead of map: you'll get good old regular optionals instead:

var shouldReturnNil: Bool? = nil
let doubleOptionalValue: String? = shouldReturnNil.flatMap { $0 ? nil : "MyValue" }
doubleOptionalValue                 // nil
doubleOptionalValue == nil          // true  πŸ‘

var shouldReturnNil: Bool? = true
let doubleOptionalValue: String? = shouldReturnNil.flatMap { $0 ? nil : "MyValue" }
doubleOptionalValue                 // Optional(nil)
doubleOptionalValue == nil          // true  πŸ‘

Distinguishing between nil and "some value, which happens to be nil", is critical and not something that can be collapsed down. Take this example:

let maybeInts: [Int?] = [1,2,nil,3]
let i: Int? = nil
// returns .some(nil)
let firstNil = maybeInts.first(where: { $0 == i })
let j = 5
// returns nil
let firstFive = maybeInts.first(where: { $0 == j })

If == nil drilled into the inner nil, how would you distinguish between successfully finding a nil entry, and finding no result?

4 Likes

Nice, that's a simple and elegant solution. I've always defaulted to using map, but now this is making me think I should default to flatMap instead.

That makes sense, thanks for spelling that out. I agree, there needs to be a way to distinguish nil values from each other depending on how nested they are.

Would it make sense for the compiler to warn against comparing multi-layer optionals to a nil literal? I.e:

let doubleOptionalValue: String?? = nil
doubleOptionalValue == nil                             // Shows warning that you might get a false negative
doubleOptionalValue == Optional<Optional<Int>>(nil)    // Doesn't show warning

Remember that nil by itself has no type, just like eg .none, so their type has to be inferred.

The current behavior makes sense:

let nestedOptional: Optional<Optional<Int>> = nil

print( nestedOptional == nil              ) // true (because type of nil is inferred to Int??)
print( nestedOptional == nil as Int?      ) // false
print( nestedOptional == nil as Int??     ) // true
print( nestedOptional == nil as Int???    ) // false
print( nestedOptional == nil as Int????   ) // false
print( nestedOptional == nil as Int?????  ) // false
print( nestedOptional == nil as Int?????? ) // false

print("First three written with less special syntax:")
print(  nestedOptional == nil                           ) // true (because type of nil is inferred to Int??)
print(  nestedOptional == Optional<Int>(nil)            ) // false
print(  nestedOptional == Optional<Optional<Int>>(nil)  ) // true

print("First three written with no special syntax:")
print(  nestedOptional == .none                         ) // true (because type of .none is inferred to Int??)
print(  nestedOptional == Optional<Int>.none            ) // false
print(  nestedOptional == Optional<Optional<Int>>.none  ) // true


Also, by the logic of your suggested warning, you would get a warning on the first line of your example too, right? : )

I think a lot of the confusion around (nested) optionals comes from all the special syntax/sugar that has been added in order to make them more convenient. I don't know if the conveniences could be more consistent in order to lessen the confusion.

3 Likes

Hehe, true, the first line would have a warning too! Which I wouldn't be opposed to. I honestly can't remember the last time I wanted to actually use a double-optional type (the bug which led me to create this post was due to an accidental double-optional). That's why I like @gwendal.roue's suggestion to use flatMap and basically avoid nested optionals altogether.

Speaking of nested optionals, here's some more grist to the mill…

1 Like