Search code examples
swiftxcodefunctional-programmingswift4.1

compactMap behaves differently when storing in an optional variable


Consider the following array.

let marks = ["86", "45", "thiry six", "76"]

I've explained my doubts in the following two cases.

Case#1

// Compact map without optionals
let compactMapped: [Int] = marks.compactMap { Int($0) }
print("\(compactMapped)")

Result - [86, 45, 76]

Case#2

// Compact map with optionals
let compactMappedOptional: [Int?] = marks.compactMap { Int($0) }
print("\(compactMappedOptional)")

Result - [Optional(86), Optional(45), nil, Optional(76)]

Why there is "nil" in the result of Case#2? Can anyone explain why is it not like this [Optional(86), Optional(45), Optional(76)] in Case#2? (PFA playground)

enter image description here


Solution

  • I submitted this behavior as a bug at bugs.swift.org, and it came back as "works as intended." I had to give the response some thought in order to find a way to explain it to you; I think this re-expresses it pretty accurately and clearly. Here we go!

    To see what's going on here, let's write something like compactMap ourselves. Pretend that compactMap does three things:

    1. Maps the original array through the given transform, which is expected to produce Optionals; in this particular example, it produces Int? elements.

    2. Filters out nils.

    3. Force unwraps the Optionals (safe because there are now no nils).

    So here's the "normal" behavior, decomposed into this way of understanding it:

    let marks = ["86", "45", "thiry six", "76"]
    let result = marks.map { element -> Int? in
        return Int(element)
    }.filter { element in
        return element != nil
    }.map { element in
        return element!
    }
    

    Okay, but in your example, the cast to [Int?] tells compactMap to output Int?, which means that its first map must produce Int??.

    let result3 = marks.map { element -> Int?? in
        return Int(element) // wrapped in extra Optional!
    }.filter { element in
        return element != nil
    }.map { element in
        return element!
    }
    

    So the first map produces double-wrapped Optionals, namely Optional(Optional(86)), Optional(Optional(45)), Optional(nil), Optional(Optional(76)).

    None of those is nil, so they all pass thru the filter, and then they are all unwrapped once to give the result you're printing out.

    The Swift expert who responded to my report admitted that there is something counterintuitive about this, but it's the price we pay for the automatic behavior where assigning into an Optional performs automatic wrapping. In other words, you can say

    let i : Int? = 1
    

    because 1 is wrapped in an Optional for you on the way into the assignment. Your [Int?] cast asks for the very same sort of behavior.

    The workaround is to specify the transform's output type yourself, explicitly:

    let result3 = marks.compactMap {element -> Int? in Int(element) }
    

    That prevents the compiler from drawing its own conclusions about what the output type of the map function should be. Problem solved.

    [You might also want to look at the WWDC 2020 video on type inference in Swift.]