Swift Flatmap on Array with Elements Are Optional Has Different Behavior

Swift flatMap on array with elements are optional has different behavior

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.

Swift flatMap gives unexpected result while using with optional array

The issue is that for the purposes of map and flatMap, optionals are collections of 0 or 1 elements. You can directly call map and flatMap on optionals without unwrapping them:

let foo: Int? = 5
foo.map { $0 * $0 } // Int? = 25; "collection of one element"
let bar: Int? = nil
bar.map { $0 * $0 } // Int? = nil; "collection of zero elements"

To illustrate in more familiar terms your current situation, you're looking at the equivalent of this:

class Person {
let cars: [[String]]
}

If you had a var persons: [Person] and called persons.flatMap { $0.cars }, the resulting type of this operation would unquestionably be [[String]]: you start out with three layers of collections and you end up with two.

This is also effectively what is happening with [String]? instead of [[String]].

In the case that you are describing, I would advise dropping the optional and using empty arrays. I'm not sure that the distinction between a nil array and an empty array is truly necessary in your case: my interpretation is that a nil array means that the person is incapable of owning a car, whereas an empty array means that the person is capable of owning a car but doesn't have any.

If you cannot drop the optional, then you will need to call flatMap twice to flatten two layers instead of only one:

persons.flatMap { $0.cars }.flatMap { $0 }

Why Does ArrayAny.flatMap() Wrap the Transform Closure Arguments in an Optional?

Array has two flatMap methods:

/// Returns an array containing the concatenated results of calling the
/// given transformation with each element of this sequence.
public func flatMap<SegmentOfResult : Sequence>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Iterator.Element]

and

/// Returns an array containing the non-`nil` results of calling the given
/// transformation with each element of this sequence.
public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]

In

var array = [[1, 2], [3, 4], [5, 6]]
// ...
print("Result: \(array.flatMap{ echo($0) })\n\n")
// Result: [1, 2, 3, 4, 5, 6]

the first method is called, because the array element type is known
to be an array and thus a Sequence. echo($0) is called with
each element, and the result is the concatenation of the inner arrays.

In

func callFlatten(_ array: Array<Any>) {
print("Input: \(array)")
print("Result: \(array.flatMap{ echo($0) })\n\n")
}

callFlatten(array)
// Result: [[1, 2], [3, 4], [5, 6]]

nothing is known about the element type, and the second method
is called.
In order to match the return type ElementOfResult? of the closure,
the generic placeholder type of T of echo is inferred as
Any? and $0 is promoted to an optional.
The result is an array with the non-nil results of echo($0) – which are exactly the array elements again. The type of the result is Array<Any>.

Cannot convert return expression in flatMap with a meaningless expression inside

The flatMap(_:) method on Sequence currently (as of Swift 4) has two different meanings:

  • It can take a transform closure that returns an optional T?, and it will return a [T], filtering out the nil results (this overload is to be renamed to compactMap(_:) in a future version).

    public func flatMap<ElementOfResult>(
    _ transform: (Element) throws -> ElementOfResult?
    ) rethrows -> [ElementOfResult]

  • It can take a transform closure that returns a Sequence, and it will return an array containing the concatenation of all the resulting sequences.

    public func flatMap<SegmentOfResult : Sequence>(
    _ transform: (Element) throws -> SegmentOfResult
    ) rethrows -> [SegmentOfResult.Element]

Now, in Swift 4, String became a RangeReplaceableCollection (and therefore a Sequence). So Swift 3 code that did this:

// returns ["foo"], as using the `nil` filtering flatMap, the elements in the closure
// are implicitly promoted to optional strings.
["foo"].flatMap { $0 }

now does this:

// returns ["f", "o", "o"], a [Character], as using the Sequence concatenation flatMap,
// as String is now a Sequence (compiler favours this overload as it avoids the implicit
// conversion from String to String?)
["foo"].flatMap { $0 }

To preserve source compatibility, specialised flatMap overloads were added for strings:

//===----------------------------------------------------------------------===//
// The following overloads of flatMap are carefully crafted to allow the code
// like the following:
// ["hello"].flatMap { $0 }
// return an array of strings without any type context in Swift 3 mode, at the
// same time allowing the following code snippet to compile:
// [0, 1].flatMap { x in
// if String(x) == "foo" { return "bar" } else { return nil }
// }
// Note that the second overload is declared on a more specific protocol.
// See: test/stdlib/StringFlatMap.swift for tests.
extension Sequence {
@_inlineable // FIXME(sil-serialize-all)
@available(swift, obsoleted: 4)
public func flatMap(
_ transform: (Element) throws -> String
) rethrows -> [String] {
return try map(transform)
}
}

extension Collection {
@_inlineable // FIXME(sil-serialize-all)
public func flatMap(
_ transform: (Element) throws -> String?
) rethrows -> [String] {
return try _flatMap(transform)
}
}

Such that the above usage would still return a [String] in Swift 3 compatibility mode, but a [Character] in Swift 4.

So, why does

let array = [1, 2, 3, 4, 5, 6]

array
.flatMap {
print("DD")
return $0 // Cannot convert return expression of type 'Int' to return type 'String?'
}
.forEach {
print("SS")
print($0)
}

tell you that the closure should return a String??

Well, Swift currently doesn't infer parameter and return types for multi-statement closures (see this Q&A for more info). So the flatMap(_:) overloads where the closure returns either a generic T? or a generic S : Sequence aren't eligible to be called without explicit type annotations, as they would require type inference to satisfy the generic placeholders.

Thus, the only overload that is eligible, is the special String source compatibility one, so the compiler is expecting the closure to return a String?.

To fix this, you can explicitly annotate the return type of the closure:

array
.flatMap { i -> Int? in
print("DD")
return i
}
.forEach {
print("SS")
print($0)
}

But if you're not actually using the optional filtering functionality of this flatMap(_:) overload in your real code, you should use map(_:) instead.

Swift 4.2 weird -.map- behavior

The way map works in Swift (and elsewhere) is that it can map (change) input from one type to another. So in your case, you will call map on your [CBPeripheral] and for each element, the map function will return another value. So, if the identifier is a String, you will end out with an array of Strings.

That is how it should work.

However :)

In your case, you have defined your mapping function like so

(perif) -> Void

Meaning that you will run through your array and call map, with a function that returns nothing. Meaning that the outcome will be an array filled with Void values.

Therefore, as others are suggesting in the comments, if you just want to examine the content of your cbPerifList, you are better of using a for in loop like so

for cbPerif in cbPerifList {
print(cbPerif.identifier)
}

If however, you're interested in getting an array of the names of your CBPeripherals you can do something like this

let names = cbPerifList.map { $0.identifier }

A comment to the above: To read more about how we can end up just writing { $0.name } have a look at the chapter about Closures in the Swift documentation, or this answer

Hope that helps you.

Creating an extension to filter nils from an Array in Swift

It's not possible to restrict the type defined for a generic struct or class - the array is designed to work with any type, so you cannot add a method that works for a subset of types. Type constraints can only be specified when declaring the generic type

The only way to achieve what you need is by creating either a global function or a static method - in the latter case:

extension Array {
static func filterNils(_ array: [Element?]) -> [Element] {
return array.filter { $0 != nil }.map { $0! }
}
}

var array:[Int?] = [1, nil, 2, 3, nil]

Array.filterNils(array)

Or simply use compactMap (previously flatMap), which can be used to remove all nil values:

[1, 2, nil, 4].compactMap { $0 } // Returns [1, 2, 4]

How to use flatMap() and reduce() on a range of a 2d array

You could flatten the array of arrays and then reduce that. For example:

let total = array[row-1...row+1]
.flatMap { $0[col-1...col+1] }
.reduce(0, +)

That yields:

51


If you wanted to avoid the overhead of building that flattened array (which is immaterial unless dealing with very large subarrays, i.e., millions of values), you can perform the calculation lazily:

let total = array[row-1...row+1]
.lazy
.flatMap { $0[col-1...col+1] }
.reduce(0, +)

For-in loop goes one-too-far and finds 'nil' while unwrapping

Basically you've done something you're not supposed to do, and thus you've stumbled on something that's probably a compiler bug (because the compiler didn't stop you). Very nice!

Now the solution. You are evidently trying to unwrap with your Int!. To unwrap safely, use for case syntax:

let list:[Int?] = [1,2,3,4,5,6,7]
for case let x? in list
{
print(x) // 1, 2, 3, 4, 5, 6, 7
}

Another way (same result):

let list:[Int?] = [1,2,3,4,5,6,7]
list.flatMap{$0}.forEach{print($0)} // 1, 2, 3, 4, 5, 6, 7


Related Topics



Leave a reply



Submit