Meaning of Optional <Optional <Optional <T>>>

Given:

Optional <T> // nil or T
Optional <Optional <T>>

Optional <Optional <Optional <T>>>

Optional <Optional <Optional <Optional <T>>>>
...

Would it be incorrect to say "They are, effectively, all Optional <T>" ?

Also, what's the easiest way to fully unwrap a nested one?

Here is a practical case, motivated by this post:

let u: Optional <String> = "23"
let v: Optional <Optional <Int>> = u.map {Int ($0)}
if let v {
    if let v {
        print ("number:", v)
    }
    else {
        print ("number:", "nil")
    }
}
else {
    print ("number:", "nil")
}

…well there’s always this: Challenge: Flattening nested optionals

(Of course if you know what the base type should be then you can just write x as? T)

2 Likes

It would indeed be incorrect to say that! One of the oldest Swift blog posts, before swift.org even existed (and unfortunately it looks like Apple didn't save those old blog posts). Fortunately, the Wayback Machine provides:

1 Like

Thank you @Danny

But, how does this automagically work?

let v: Optional <Optional <Optional <Optional <Optional <Int>>>>> = 23
print (v as? Int)

// prints Optional(23)

Dynamic casts on optionals recursively try to cast the unwrapped value when there is one.

You have .some(.some(.some(.some(.some(23))))), and you're trying to cast it to Int. The cast algorithm sees the outermost level is .some(x), so it tries to cast x to Int. x is .some(.some(.some(.some(23)))), so the cast algorithm sees the next level is .some(y), so it tries to cast y to Int. And that just continues on down until it sees that it actually has an Int and the cast succeeds. You get an Optional at the end because that's how as? reports success or failure, by returning an optional.

5 Likes

Think it becomes something like:

enum NestedOptional<T> {
   case some(T)
   case noneNone
   case none
}

Question is though does it make sense to have such type? I think yes, sometimes helpful to distinguish where exactly it’s none.

1 Like

There is actually a very nice use case for Optional<Optional>. Consider:



struct Container<T> {
    let `default`: T
    let firstOverride: T??
    let secondOverride: T?
    
    var contents: T {
        if let firstOverride {
            print("first override")
            return firstOverride!
        }
        
        if let secondOverride{
            print("second override")
            return secondOverride
        }
        
        print("default")
        return `default`
    }
}

let container1 = Container<Int?>(
    default: .none,
    firstOverride: .some(.some(.none)),
    secondOverride: .none
);

let container2 = Container<Int?>(
    default: 5,
    firstOverride: nil,
    secondOverride: .some(.some(6))
);

/// prints:
/// first override
/// nil
let num: Int? = container1.contents
print(num)

/// prints:
/// second override
/// Optional(6)
let num2: Int? = container2.contents
print(num2)

Force-unwrapping firstOverride! literally cannot crash ever, because what you are effective doing is converting .some(.some(.none)) into .some(.none) in the if let, and then when you force unwrap it simply becomes .none i.e. nil. Even though all of these are technically equivalent to nil once they get resolved by the runtime, at first they are something else entirely, under the hood... just values of the Optional<T> enumeration.

This kind of madness is necessary because it's the only way for T to represent an optional type.

As to why this is actually useful and a good thing, it came in really handy when I had to design a dependency injection system, and we needed to inject on optional properties sometimes. What if you want to inject nil as an override for some default value? This is the way.

1 Like

There are times when T?? is meaningful.

Let say you have a tri-state boolean: true, false, and unselected. A way to represent this is with Bool?. Now, let say you have an array of these tri-states, [Bool?]. Then array.first returns Bool??

  • array.first == .none: the array is empty
  • array.first == .some(.none): the first element is unselected
  • array.first == .some(.some(true)): the first element is true
  • array.first == .some(.some(false)): the first element is false

The code could be something like

if let triState = array.first, triState == nil {
    // The first item in the array is unselected.
}

Personally, I think it's a bad design. I would prefer an enum with all the states instead of abusing Bool?, but eh, whatever. This is a possible way to do it.

1 Like

Optionals of optionals are important because sometimes code that's generic over some type T has to work with a T?, and you don't want that to have weird, non-composable behavior just because T happens to already be an optional type.

Like, if I have dict: [String: T], and I look up dict["hello"], I'm going to get a T? back. The composable optionals-of-optionals design means that that T? being nil reflects nothing more than the dynamic absence of an entry for "hello" in that dictionary. As long as there is, the T payload will be exactly the value stored in the dictionary for that key. An automatically-folding-optionals design would mean that a nil T? would mean either that there's no entry for the key or that the entry is present but T happens to be an optional type and the entry happens to have a nil value. It is just a much harder design to statically reason about in every possible way.

Otherwise, I do not recommend using optionals of optionals directly. As Jeffery suggests, if you have a type with more than one semantically-meaningful kind of absence, you should probably make a dedicated enum for it.

10 Likes

To illustrate, consider how optional chains work:

foo?.bar?.baz?.frob

If frob has type Int, this gives you an Int? at the end of the expression. What you can tell from the value is either all of foo, bar and baz contained a value, or that some unspecified component of it was nil.

There is an alternate world where each ?. wraps in another level of Optional. In this case, foo?.bar?.baz?.frob would give you an Int???. The difference is that now, the level at which you have .none tells you which value was nil:

  • Int???.none: foo was nil
  • .some(.none): foo had a value but bar was nil
  • .some(.some(.none)): foo.bar had a value but baz was nil
  • .some(.some(.some(let frob))): foo.bar.baz was non-nil, so you get the value of frob

People are rarely after that level of specificity, so collapsing to one level of Optional is usually fine. But sometimes, people want to be more specific about certain things, and in those cases, nesting Optionals can give you that extra bit of information.

4 Likes