Recursively Unwrap `Optional` on Explicit Unwrapping/Optional Chaining

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.

2 Likes

Solution 4: Provide an explicit “unwrap as much as possible” method

We can’t express this in the language today, but essentially:

extension Optional {
  typealias UltimatelyWrappedNonoptionalType = (Wrapped == Optional) ? Wrapped.UltimatelyWrappedNonoptionalType : Wrapped

  func asSingleOptional() -> Self.UltimatelyWrappedNonoptionalType? {
    return self as? Self.UltimatelyWrappedNonoptionalType
  }
}
2 Likes

Agreed that that is one solution.

Though this form requires that classes/struct are able to inspect its associated type.
This kind of meta-computation can open the floodgate to many things.
We can avoid this by having the compiler do it, to avoid adding this typealias syntax to the language.

Nevertheless I think the current implicit behaviour may not be what many programmers expect, and that may need to change that as well.

I agree with Nevin.

Nested optionals are one of those things that you do actually need when you need them, and having auto-flatten behavior everywhere would be actively harmful in those cases. At the same time, as you mention, they are confusing to beginners, and usually not what you want.

It seems like the best option might be to create a method on optional which flattens one or more layers. As Nevin mentions, this would require something the type system can't express at the moment, but which would be a useful construct in general (I have run into the same problem of expression with optionals in other cases).

Then your example would be:

guard let first = a.first.flattened else {
    // statement 1/2
}
1 Like

Was this ever solved? I have a generic bit of code that is getting tripped up with double-optional wrapping.


@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T

    init(_ key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
			guard newValue as T? != nil else {
				UserDefaults.standard.removeObject(forKey: key)
				return
			}
			UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

the guard block in this is being tripped up because my newValue in this case is nil, but Swift compiles that guard expression away.

newValue has type T, so newValue as T? is just Optional.some(newValue) and can never be nil. I think you're trying to retroactively make newValue be optional, and that's just not how anything works.

2 Likes

Right, so how would you recommend I write this?

I want an @propertyWrapper for UserDefaults that can handle optional or non-optional values.

I'm reading from defaults, so I can't trust that defaults doesn't have nil in it.

I'm choosing for this particular key, that nil has meaning, and I want to be able to reset it.

Do I need to write two different @propertyWrapper for nullable vs non-nullable properties?

@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T

    init(_ key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
			UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

@propertyWrapper
struct UserDefaultNullable<T> {
    let key: String
    let defaultValue: T?

    init(_ key: String, defaultValue: T?) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var wrappedValue: T? {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
			guard let value = newValue else {
				UserDefaults.standard.removeObject(forKey: key)
				return
			}
			UserDefaults.standard.set(value, forKey: key)
        }
    }
}

This works^ but feels silly

I would suggest giving your wrapper a projection with a reset method, or maybe just making it a method on the wrapper if you're okay with only a limited scope being able to reset to default.

2 Likes