Home > Back-end >  Why do `map(foo)` and `map{ foo($0) }` return different results?
Why do `map(foo)` and `map{ foo($0) }` return different results?

Time:03-30

For the following code snippet, why do res1 and res2 have different values?

func test() {
    let a: String? = nil
    let b: String? = nil

    let foo: (String?) -> Int = { $0 == nil ? 0 : 1}

    let res1 = [a, b].compactMap { $0 }.map(foo)
    let res2 = [a, b].compactMap { $0 }.map { foo($0) }

    print("res1: ", res1)
    print("res2: ", res2)
}

The output is

res1:  [0, 0]
res2:  []

CodePudding user response:

This appears to be a result in how the compiler selects the types of [a, b].compactMap based on the downstream operation. You can see this by inspecting the types of the arguments as they pass through the functions:

func printType<T>(_ i: String, _ v: T) -> T {
  print(i, "->", T.self)
  return v
}

func test() {
    let a: String? = nil
    let b: String? = nil

    let foo: (String?) -> Int = { $0 == nil ? 0 : 1 }

    let res1 = printType("cm1", printType("a1", [a, b]).compactMap { $0 }).map(foo)
    let res2 = printType("cm2", printType("a2", [a, b]).compactMap { $0 }).map { foo($0) }

    print("res1: ", res1)
    print("res2: ", res2)
}

test()
// a1 -> Array<Optional<Optional<String>>>
// cm1 -> Array<Optional<String>>
// a2 -> Array<Optional<String>>
// cm2 -> Array<String>
// res1:  [0, 0]
// res2:  []

It appears that:

  1. In the case of res1:
    • Because foo takes a String, map(foo) is typed such that a String? is passed through — and for map to be receiving a String?, compactMap must be returning a [String?]
    • In order for compactMap to be returning a [String?], its input must be a [String??]
    • Although [a, b] defaults to being a [String?], the compiler can also implicitly upcast it to a [String??], so that's what it does
    • Hence, only one layer of optionality is removed from [String??], and each of the String? values is passed to the map and into foo
  2. In the case of res2, the compiler isn't restricted as heavily by map { foo($0) }, since $0 can be implicitly upcast inside of the closure before being passed to foo, and this is what the compiler prefers:
    • $0 is a String which is upcast to String? before being passed to foo
    • For map to receive String values, compactMap must return [String]
    • Since compactMap is returning a [String], its input must be [String?], which [a, b] is, no upcasting needed
    • Hence, now compactMap is actually filtering the nils out of [String?] into [String], and since no values are left, foo is never even called (you can see this with more print statements inside of foo

This sort of thing is very situational in the compiler, and you happened to have found a specific case where this happens. For instance, the compiler parses the results of single-statement closures differently from multi-statement closures: if you turn compactMap { $0 } into compactMap { _ = 1 1; return $0 }, the closure will parsed differently, and type checking will occur in a different order, resulting in [] in both cases:

let res1 = printType("cm1", printType("a1", [a, b]).compactMap { _ = 1   1; return $0 }).map(foo)
let res2 = printType("cm2", printType("a2", [a, b]).compactMap { _ = 1   1; return $0 }).map { foo($0) }

// a1 -> Array<Optional<String>>
// cm1 -> Array<Optional<String>>
// a2 -> Array<Optional<String>>
// cm2 -> Array<Optional<String>>
// res1:  [0, 0]
// res2:  [0, 0]

In this case, the compiler actually ended up preferring the surprising case in both instances, since return $0 allows the compiler to upcast from String?String?? (and then filter back down from [String??][String?])!

Either way, this seems worthy of a report on https://bugs.swift.org, since the behavior is incredibly surprising. I will be happy to add test cases and comments to the report if you go ahead and file.

  • Related