Weird Behavior With compactMap

The collection function compactMap is the replacement for a particular use of flatMap, and was introduced in Swift 4.1. My understanding of compactMap is that it applies a closure to the elements of a sequence and removes nil results, leaving only non-nil values in the sequence. This is how I read its type signature from the documentation:

func compactMap<ElementOfResult>(_ transform: (Base.Element.Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]

But based on some testing, things aren’t quite that simple or predictable.

My conclusion is that depending on the Swift type-checker, compactMap will return different results in ways that are not always obvious.

The first ambiguity is that the return value depends on the type of the receiver:

let array = [2, 3, nil]

let compactArray: [Int] = array.compactMap { $0 }
let optionalCompactArray: [Int?] = array.compactMap { $0 }

print(compactArray.count) // => 2
print(optionalCompactArray.count) // => 3
//printing without typed assignement
print(array.compactMap({ $0 }).count) // => 2

It appears that compactMap will return either Int or Int?, depending on type of assignment. I find this understandable, but less than ideal.

So, for the remaining examples, I will type the receiver as [Int?], so that this remains consistent:

The second ambiguity is that seemingly functionally identical code triggers different results with compactMap:

let optCompact1: [Int?] = array.compactMap {
    guard let num = $0 else { return nil }
    return num > 50 ? num : nil

let optCompact2: [Int?] = array.compactMap {
    guard let num = $0, num > 50 else { return nil }
    return num

print(optCompact1.count) // => 2
print(optCompact2.count) // => 3

The way that I understand this is that when the guard statement fails, then a true nil is returned, where as the compactMap behaves as though the second line causes it to return an unwrapped optional.

This seems bad and very difficult to predict. It seems to me that it should be treated as though it were returning an unwrapped optional, and the nil values were removed.

Likewise, if the code is put in a function and that function is called from the closure, then it treats it as an unwrapped optional:

func func1(_ n: Int?) -> Int? {
    guard let num = n else { return nil }
    return num > 50 ? num : nil
func func2(_ n: Int?) -> Int? {
    guard let num = n, num > 50 else { return nil }
    return num

let optCompact1b: [Int?] = array.compactMap { func1($0) }
let optCompact2b: [Int?] = array.compactMap { func2($0) }

print(optCompact1b.count) // => 3
print(optCompact2b.count) // => 3

Lastly, if we rewrite this as closures which are either called directly, or directly applied, then we get the following results, which are both inconsistent with each other, and inconsistent with the initial results:

let closure: ((Int?)->(Int?)) = {(n: Int?) in
    guard let num = n else { return nil }
    return num > 50 ? num : nil

let closure2: ((Int?)->(Int?)) = {(n: Int?) in
    guard let num = n, num > 50 else { return nil }
    return num

let optCompact1c: [Int?] = array.compactMap { closure($0) }
let optCompact2c: [Int?] = array.compactMap { closure2($0) }
let optCompact1d: [Int?] = array.compactMap(closure)
let optCompact2d: [Int?] = array.compactMap(closure2)

print(optCompact1c.count) // => 3
print(optCompact2c.count) // => 3

print(optCompact1d.count) // => 0
print(optCompact2d.count) // => 0

This seems really wrong to me, but perhaps I simply do not understand the differences between how the following cases above relate to wrapped and unwrapped optionals. Anyone want to weigh in?


Interesting Gotcha With UIButton Title

I was writing some code along these lines to set the background of a view based upon the value of the title of a button.1 Seems simple enough. The otherView.background should definitely be green at the end of this code. But it wasn’t. Can you figure out what I did wrong?

// starting condition - sender.titleLabel?.text == "invalidValue"

button.setTitle("validValue", for: .normal)

if sender.titleLabel?.text == "validValue" {
    otherView.backgroundColor = .green
} else {
    otherView.backgroundColor = .red

Turns out that setting the text of the titleLabel is an asynchronous, animated event. When we call setTitle(_, for:), we are setting the underlying title for a given state, and we also trigger an animated change to the visible titleLabel.text . So even if you call setNeedsDisplay or setNeedsLayout on the button prior to reading its value, titleLabel.text will not reflect the new value until the end of the animated change.

So, how do we get the correct value in an instant? Use button.title(for: .normal) 2 This reflects the “view model” value for the title in a given state, whereas the titleLabel.text value is what is actually displayed to the user at that instant.

  1. Actual example is more complex, but this gets to the heart of it. ↩︎
  2. replace .normal with a different state, if that is relevant ↩︎