Double optionals, let, and?

I ran into something just now I've never experienced in years of Swift development: let and ?? only seem to strip away one optional wrapper. Take this example:

struct
Foo
{
	let hashtags: [String?]
}

let foo = Foo(hashtags: ["#foo"])


let bar = foo.hashtags.first ?? "<none>"
print("hashtags: \(bar)")

prints

hashtags: Optional("#foo")

This already seems extraordinarily counter-intuitive. The compiler is even fooled by it; if you write print("\(foo.hashtags.first ?? "<none>"), the compiler warns the expression produces an optional and offers a fix-it to add ?? "" to the end. But if you do that, it doesn’t fix the problem! You have to put parentheses around the first ?? to make it work.

Similarly if let bar = foo.hashtags.first produces an optional. Not something I would have expected.

It’s almost certainly too late to fix this now; it would break some source. But it seems like a pretty nasty wart. Can anyone explain to me why it's not? Thanks.

If you are interested in the inner optionality this make sens. Defaulting to unwrapping both optional would make some code impossible to write.

var bar = foo.hashtags.first ?? "none"
...
bar = nil

While unwrapping twice might look counter-intuitive well you are unwrapping twice.

IIRC thought if/guard let try? someTrowingAndOptionalFunc() is flattened

1 Like

It's logical behaviour. Optional is a box. You can put an Optional in an Optional, you can put a box in a box. And sometimes that distinction is important. If the distinction is not important, then often the client API is poor for allowing the double optional to exist at all.

3 Likes

I would personally argue that this makes a total sense semantically: if you declare an array of optional items, there's a distinction between it being empty (that is, foo.hashtags.first returning a nil) and it being filled with some items, where perhaps only the first one is nil (and thus foo.hashtags.first returning an Optional(nil)). If operators flattened the optional completely, you wouldn't be able to differentiate the cases (you might use different APIs for this if such are available, but that's beyond the point) — so if nested optionality is problematic, then you don't need a [String?], just use a [String].

6 Likes

For what it's worth, throwing a flatMap in there will give you the desired behavior:

let bar = foo.hashtags.first.flatMap {$0} ?? "<none>"

I'm glad nested optionals are properly supported in Swift, and like the current situation.

However, I agree that the ?? may be somewhat surprising for some users, when using a non-optional on the right-hand-side of the operator. ?? only works for T on the rhs if Optional<T> is on the lhs. Therefore, Optional<Optional<T>> on the lhs should require an Optional<T> on the rhs.

However, Swift also supports implicit optional promotion. It makes sense, really, that you can pass a known non-nil where an optional is expected. It would be a hassle to always have to wrap your known non-nil values in .some(...) as function parameters, etc.

So in this case, your expression really becomes:

let bar = foo.hashtags.first ?? .some("<none>")

… which is clearly an optional.

Maybe the compiler should generate a warning when optional promotion and nested optionals are combined?

I think this is the crux of my complaint about this. I expect to get what I see on the RHS of ??. I'd prefer an error, or at least a warning that my non-optional RHS was wrapped.

Or alternatively

let bar = foo.hashtags.first ?? nil ?? "<none>"
Terms of Service

Privacy Policy

Cookie Policy