Swift flatMap on array with elements are optional has different behavior
Asked Answered
E

2

2
let arr: [Int?] = [1,2,3,4,nil]

let arr1 = arr.flatMap { next in
    next
}
// arr1: [1,2,3,4]
let arr2: [Int?] = arr.flatMap { next -> Int? in
   next
}
// arr2: [Optional(1), Optional(2), Optional(3), Optional(4)]

I'm confused by these code, why do they make a difference?

update: please see these codes, I

let arr: [Int?] = [1,2,3,4,nil]

let arr1: [Int?] = arr.flatMap { next in
    next
}
// arr1: [Optional(1), Optional(2), Optional(3), Optional(4), nil]
let arr2: [Int?] = arr.flatMap { next -> Int? in
    next
}
// arr2: [Optional(1), Optional(2), Optional(3), Optional(4)]
Electrometer answered 29/5, 2016 at 3:10 Comment(0)
B
4

As @Adam says, it's due to the explicit type that you're supplying for your result. In your second example, this is leading to confusion caused by double wrapped optionals. To better understand the problem, let's take a look at the flatMap function signature.

@warn_unused_result
public func flatMap<T>(@noescape transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]

When you explicitly specify that the result is of type [Int?], because flatMap returns the generic type [T] – Swift will infer T to be Int?.

Now this causes confusion because the closure that your pass to flatMap takes an element input and returns T?. Because T is Int?, this closure is now going to be returning T?? (a double wrapped optional). This compiles fine because types can be freely promoted to optionals, including optionals being promoted to double optionals.

So what's happening is that your Int? elements in the array are getting promoted to Int?? elements, and then flatMap is unwrapping them back down to Int?. This means that nil elements can't get filtered out from your arr1 as they're getting doubly wrapped, and flatMap is only operating on the second layer of wrapping.

Why arr2 is able to have nil filtered out of it appears to be as a result of the promotion of the closure that you pass to flatMap. Because you explicitly annotate the return type of the closure to be Int?, the closure will get implicitly promoted from (Element) -> Int? to (Element) -> Int?? (closure return types can get freely promoted in the same way as other types) – rather than the element itself being promoted from Int? to Int??, as without the type annotation the closure would be inferred to be (Element) -> Int??.

This quirk appears to allow nil to avoid being double wrapped, and therefore allowing flatMap to filter it out (not entirely sure if this is expected behaviour or not).

You can see this behaviour in the example below:

func optionalIntArrayWithElement(closure: () -> Int??) -> [Int?] {
    let c = closure() // of type Int??
    if let c = c { // of type Int?
        return [c]
    } else {
        return []
    }
}

// another quirk: if you don't explicitly define the type of the optional (i.e write 'nil'),
// then nil won't get double wrapped in either circumstance
let elementA : () -> Int? = {Optional<Int>.None} // () -> Int?
let elementB : () -> Int?? = {Optional<Int>.None} // () -> Int??

// (1) nil gets picked up by the if let, as the closure gets implicitly upcast from () -> Int? to () -> Int??
let arr = optionalIntArrayWithElement(elementA)

// (2) nil doesn't get picked up by the if let as the element itself gets promoted to a double wrapped optional
let arr2 = optionalIntArrayWithElement(elementB)

if arr.isEmpty {
    print("nil was filtered out of arr") // this prints
}

if arr2.isEmpty {
    print("nil was filtered out of arr2") // this doesn't print
}

Moral of the story

Steer away from double wrapped optionals, they can give you super confusing behaviour!

If you're using flatMap, then you should be expecting to get back [Int] if you pass in [Int?]. If you want to keep the optionality of the elements, then use map instead.

Bodyguard answered 29/5, 2016 at 11:16 Comment(9)
Thanks. Well explained!Electrometer
This line: ...the closure will get implicitly promoted from (Element) -> Int? to (Element) -> Int??,should it be corrected as: the closure will get implicitly promoted from (Element) -> Int? to ((Element) -> Int??) for more precise?Electrometer
@Electrometer No it's correct how it is – you're explicitly annotating the closure as (Element) -> Int?, but flatMap is expecting (Element) -> T?. In this case T is inferred to be Int?, therefore T? is Int??, so the closure is (Element) -> Int??. The closure itself isn't optional, only the return type.Bodyguard
as you say above, the second transform closure is equal to the first one? They are both (Element) -> Int?? ?Electrometer
@Electrometer Types can be freely promoted to optionals, and this applies to closures too, so therefore a (Element) -> Int? can be promoted to a (Element) -> Int??. In the same way a () -> Int can be promoted to a () -> Int?. In this example you're creating a (Element) -> Int?, but flatMap is expecting a (Element) -> Int??. This isn't a problem though as the closure you create can be promoted.Bodyguard
Understood, thanks again. I'll look more from you code example.Electrometer
Sorry for asking here it may be duplicate . var newTemp : [[Int]?] = [[123],nil,[456]] now var output = newTemp.flatMap{$0} it shows [[123], [456]] not [123,456] why ?Indenture
@PrashantTukadiya There are two flavours of flatMap on sequences; one that flattens out sequences returned from the closure passed to it, and one that filters out nils from the closure passed to it (note this overload is soon to be renamed to compactMap to avoid confusion). In your example, you’re using the nil filtering overload, so the nils in your array are filtered out. It won’t however do any flattening of the nested arrays. You could call flatMap again to achieve that.Bodyguard
@Bodyguard You are awesome !!Indenture
T
1

It has to do with the explicit type you are giving arr2. When you specify that arr2 must be of type [Int?], Swift complies and simply wraps the values in an optional. However, the flatMap operation returns the non-nil values of the array, which makes it obvious as to why arr1 is of type [Int]. To see that flatMap returns the non-nil values of the array just look at the declaration notes of the function. Also, if you understand what the flatMap operation does in general use, which is that it unwraps the inner wrapping of values, you can see why the function returns a value of type [Int]. Consider arr, which is of type [Int?]. Rewriting the type of arr in its wrapped form, it becomes Array<Optional<Int>>. For the flatMap function to unwrap the inner wrapping, the function must return a value of type Array<Int>. To do this, the function must throw out nil values.

Tetartohedral answered 29/5, 2016 at 3:25 Comment(4)
I don't know if you notice the difference of the trailing transform function parameter and return type, the second explicitly write out the return type is (Int?) , but the first one not, this is the part I can't figure out why mostly.Electrometer
The return types for both transforms are identical. The only difference is one is implied. The cause of the different types of the two arrays is the explicit type given before the assignment (=) operator in the second case.Tetartohedral
For arr1, the return type of the transform is easily inferred by Swift because it knows you are returning a value of type Int?.Tetartohedral
why arr1 didn't fliter the nil?Electrometer

© 2022 - 2024 — McMap. All rights reserved.