14 · Optional of an optional
Int?? from a dictionary of optionals, ?? and if let peeling one layer, map vs flatMap, try? flattening, optional chaining, and an isNil you write yourself.
Shape: what does this print · Reported: Swiggy — define Optional's type and its cases, then implement isNil() · Verified: run with Swift 6.4
"A dictionary where some people have no nickname. Tell me what each line prints — types included."
let nicknames: [String: String?] = ["ana": "Annie", "ben": nil]
let ana = nicknames["ana"]
let ben = nicknames["ben"]
let cleo = nicknames["cleo"]
print(ana as Any, ben as Any, cleo as Any) // ①
print(type(of: ben), ben == nil, cleo == nil) // ②
print((ben ?? "no key") as Any, (cleo ?? "no key") as Any) // ③
print((ben ?? nil) ?? "no nickname") // ④
if let value = ben {
print("found key, value is", value as Any) // ⑤
} else {
print("no such key")
}
let digits: String? = "42"
let mapped = digits.map { Int($0) }
let flat = digits.flatMap { Int($0) }
print(type(of: mapped), type(of: flat)) // ⑥
enum ParseError: Error { case bad }
func parse(_ text: String) throws -> Int? {
guard !text.isEmpty else { throw ParseError.bad }
return Int(text)
}
let attempt = try? parse("x")
print(type(of: attempt), attempt as Any) // ⑦
struct Profile { var nickname: String? }
struct User { var profile: Profile? }
let user: User? = User(profile: Profile(nickname: nil))
print(type(of: user?.profile?.nickname)) // ⑧
extension Optional {
var isNil: Bool {
if case .none = self { return true }
return false
}
}
print(ben.isNil, cleo.isNil, ben?.isNil as Any) // ⑨A hint, if you're stuck
- A dictionary subscript returns
Value?. HereValueis alreadyString?. - There are two ways to be "nothing" here: no key, or a key whose value is
nil. Which lines can tell them apart? ??,if letand?.each remove exactly one layer.Optionalis an ordinary enum. What are its two cases?
Answers (verified output)
Optional(Optional("Annie")) Optional(nil) nil
Optional<Optional<String>> false true
nil Optional("no key")
no nickname
found key, value is nil
Optional<Optional<Int>> Optional<Int>
Optional<Int> nil
Optional<String>
false true Optional(true)- ① three different shapes. The subscript wraps the stored value in one more optional, so the type is
String??.anais.some(.some("Annie")).benis.some(.none)— "the key exists, and its value is nil" — printedOptional(nil).cleois.none— no key at all. - ②
ben == nilisfalse. Only the outer layer is compared withnil, andben's outer layer is.some. This is the line that catches people. - ③
nil Optional("no key").??peels one layer. Forbenit peels to the inner value, which isnil. Forcleoit uses the default — but the result type isString?(that's the overload that fits aString??on the left), so the default comes back wrapped. - ④
no nickname. Two??s peel two layers. Quick, but it erases the difference between "no key" and "no nickname". - ⑤
found key, value is nil.if letalso peels one layer. It succeeds forbenbecause the key exists, and bindsvalueas aString?that isnil. Code that reads "found it" here and carries on is the real-world bug. - ⑥
mapwraps,flatMapflattens.Int($0)already returnsInt?.mapwraps that again:Int??.flatMapexpects the closure to return an optional and doesn't add a layer:Int?. - ⑦
Optional<Int> nil—try?flattens. Since Swift 5 (SE-0230),try?on something that returnsInt?givesInt?, notInt??. We also compiled the same file with-swift-version 4, where this line printsOptional<Optional<Int>> Optional(nil). The cost of flattening:nilhere can mean "it threw" or "it returned nil", and you can't tell which. - ⑧
Optional<String>. Optional chaining never stacks. However many?.you chain, the result has exactly one optional layer around the last property's type. - ⑨
false true Optional(true).ben.isNilchecks the outer layer:.some, sofalse.cleo.isNilistrue.ben?.isNilfirst peels one layer, then callsisNilon the innerString?, which isnil—true, wrapped by the chain intoOptional(true).
The idea behind it
Optional isn't magic. It's a plain generic enum in the standard library:
enum Optional<Wrapped> {
case none
case some(Wrapped)
}String? is shorthand for Optional<String>, and nil is shorthand for .none. Because Wrapped can be any type, it can be another optional: String?? is Optional<Optional<String>>, a box that may contain a box that may contain a string.
Think of a parcel. An empty outer box means "no parcel came" — no key. An outer box with an empty inner box means "a parcel came, and it was empty on purpose" — the key exists, set to nil. Both are "no string", but they mean different things.
Every unwrapping tool — if let, guard let, ??, ?., ! — opens exactly one box. To get the string out of String?? you need two. That's the whole chapter: count the boxes, then count the openings.
Swiggy's isNil is just a pattern match on the enum's case, as in ⑨. self == nil also works, because Swift lets any optional be compared with the nil literal.
How to make it unsurprising
- Avoid
[Key: Value?]. Use[String: String]and let "no key" mean "no nickname". If you really need three states (unknown, none, some), write an enum with three named cases — nobody has to count boxes. - Never assign
nilthrough the subscript of such a dictionary.nicknames["ben"] = nilremoves the key. To store anilvalue you need.some(nil)orupdateValue(nil, forKey:). - Prefer
flatMapwhen the transform can fail, and treat atry?on a function that already returns an optional as a review comment —do/catchkeeps the two failures apart.
What I'm really scoring
That you count layers. Every unwrapping tool opens exactly one, and ben == nil being false is the line that shows whether you do. The strong answer names the two kinds of "nothing" (no key, or a key holding nil), writes isNil as a pattern match on .none without hesitating, and knows try? flattens since Swift 5 — and what that costs.
What I'd ask next
- "
nicknames["ben"] = nil— what happens?" — The key is removed: the count goes from 2 to 1. We ran it..some(nil)orupdateValue(nil, forKey:)stores anilvalue instead. - "How do you unwrap
benfully in one statement?" —if case let name?? = ben, orif let inner = ben, let name = inner. Orben ?? nilto flatten toString?first. - "What does
!do onString??when the key is missing?" — Crashes. When the key exists withnil, a single!succeeds and gives you aString?that is stillnil. - "What is
compactMapon an array ofString??" — It drops thenils and returns[String]. On an array ofString??it peels just one layer, so a.some(nil)survives as anilelement.