1

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

Shashank Mishra
  • 1,051
  • 7
  • 18
  • I'm not using it anywhere. I'm just trying to analyze the result. – Shashank Mishra Jun 28 '20 at 17:40
  • Okay, that looks like a bug. It's easy to describe what's going on: in the second example, we are effectively calling `map` instead of `compactMap`. But I do not know why the mere cast would cause that. I've filed this with bugs.swift.org. – matt Jun 28 '20 at 19:39
  • Both of them are calling `map` under the hood... Just one filters nils out, the other doesn't, because it doesn't have to.. – timbre timbre Jun 29 '20 at 00:30
  • It's a very old behavior; I've just confirmed it in Xcode 9.2. So it may be deliberate. But I find it hard to justify; it certainly it breaks one's sense of transitivity, being a situation where `b = a; c = b` gives a different result (`c`) from `c = a`. – matt Jun 29 '20 at 01:13
  • @KirilS. Yes, that's sort of what I was thinking. It "sees" that we're going to be assigning to an `[Int?]` so it says to itself, "Oh, you're willing to accept Optionals after all, so there's no work for me to do." But that's still not the way I expect a language to behave. – matt Jun 29 '20 at 01:16
  • I think it's because you've strictly defined that you're fine with the value being either Int or Nil in the new array. – Parion Jun 29 '20 at 12:36

1 Answers1

1

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.]

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Okay, this was a lot more interesting than I was expecting! Provided both an explanation and a workaround. – matt Jun 30 '20 at 02:44