Difference Between Flatmap and Compactmap in Swift

Difference between flatMap and compactMap in Swift

The Swift standard library defines 3 overloads for flatMap function:

Sequence.flatMap<S>(_: (Element) -> S) -> [S.Element]  
Optional.flatMap<U>(_: (Wrapped) -> U?) -> U?
Sequence.flatMap<U>(_: (Element) -> U?) -> [U]

The last overload function can be misused in two ways:

Consider the following struct and array:

struct Person {
var age: Int
var name: String
}

let people = [
Person(age: 21, name: "Osame"),
Person(age: 17, name: "Masoud"),
Person(age: 20, name: "Mehdi")
]

First Way: Additional Wrapping and Unwrapping:

If you needed to get an array of ages of persons included in people array you could use two functions :

let flatMappedAges = people.flatMap({$0.age})  // prints: [21, 17, 20]
let mappedAges = people.map({$0.age}) // prints: [21, 17, 20]

In this case the map function will do the job and there is no need to use flatMap, because both produce the same result. Besides, there is a useless wrapping and unwrapping process inside this use case of flatMap.(The closure parameter wraps its returned value with an Optional and the implementation of flatMap unwraps the Optional value before returning it)

Second Way - String conformance to Collection Protocol:

Think you need to get a list of persons' name from people array. You could use the following line :

let names = people.flatMap({$0.name})  

If you were using a swift version prior to 4.0 you would get a transformed list of

["Osame", "Masoud", "Mehdi"]  

but in newer versions String conforms to Collection protocol, So, your usage of flatMap() would match the first overload function instead of the third one and would give you a flattened result of your transformed values:

["O", "s", "a", "m", "e", "M", "a", "s", "o", "u", "d", "M", "e", "h", "d", "i"]

Conclusion: They deprecated third overload of flatMap()

Because of these misuses, swift team has decided to deprecate the third overload to flatMap function. And their solution to the case where you need to to deal with Optionals so far was to introduce a new function called compactMap() which will give you the expected result.

compactMap vs flatMap, Swift

There used to be two different functions named flatMap for sequences; one to filter out nil values and one to join sequences. Now, one of these (the nil variant) has been renamed to compactMap

Issue in understanding compactMap & flatMap

compactMap should be used to filter out nil elements from an array of Optionals, while flatMap can be used to flatten out a multi-dimensional array. However, you need to do both.

let final = myArray.flatMap{$0.compactMap{$0}}

print("Result:\(final)")

Map and flatMap difference in optional unwrapping in Swift 1.2

(Remark: The answer has been updated to reflect the syntax changes in Swift 3 and later, such as the abolishment of ImplicitlyUnwrappedOptional.)

Optional.map() and Optional.flatMap() are declared as follows (I have omitted the throws/rethrows modifiers which are irrelevant here):

func map<U>(_ transform: (Wrapped) -> U) -> U?
func flatMap<U>(_ transform: (Wrapped) -> U?) -> U?

Let's consider a simplified version of your first example using “map”:

let number: Int? = 1
let res1 = number.map { $0 + 1 }
print(res1) // Optional(2)

number has the type Int? and the closure type is inferred as (Int) -> Int. U is Int, and the type of the return value is Int?. number is not nil, so it is unwrapped and passed 1 is passed to the closure. The closure returns 2 and map returns Optional(2). If number were nil then the result would be nil.

Now we consider a simplified version of your second example with “flatMap”:

let number: Int? = 1
let res2 = number.flatMap { $0 + 1 }
print(res2) // Optional(2)

flatMap expects a closure of type (Wrapped) -> U?, but { $0 + 1 } does not return an optional. In order to make it compile, the compiler converts this to

let res2 = number.flatMap { return Optional($0 + 1) }

Now the closure has type (Int) -> Int?, and U is Int again. Again, number is unwrapped and passed to the closure. The closure returns Optional(2) which is also the return value from flatMap. If number were nil or if the closure would return nil then the result would be nil.

So there is indeed no difference between these invocations:

let res1 = number.map { $0 + 1 }
let res2 = number.flatMap { $0 + 1 }

However that is not what flatMap is meant for. A more realistic example would be

func foo(_ s : String?) -> Int? {
return s.flatMap { Int($0) }
}

print(foo("1")) // Optional(1)
print(foo("x")) // nil (because `Int($0)` returns nil)
print(foo(nil)) // nil (because the argument is nil)

Generally, map takes a closure of type (Wrapped) -> U and transforms

Optional<Wrapped>.none          --> Optional<U>.none
Optional<Wrapped>.some(wrapped) --> Optional<U>.some(transform(wrapped))

flatMap takes a closure of type (Wrapped) -> U? and transforms

Optional<Wrapped>.none          --> Optional<U>.none
Optional<Wrapped>.some(wrapped) --> transform(wrapped)

Here transform(wrapped) can be Optional<U>.none as well.

If (as in your example) flatMap is called with a closure which does not return an optional then the compiler converts it to an optional automatically, and there is no difference to map anymore.

What is the best way to use map and sorted together in Swift

You can use flatMap(_:) with replacingOccurrences(of:with:) to ignore the nil result while converting string to number and then sort the result array.

let array = ["1,8", "3,5", "2,5", "4"]
let sortedArray = array.flatMap({ Double($0.replacingOccurrences(of: ",", with: ".")) }).sorted()
print(sortedArray) //[1.8, 2.5, 3.5, 4.0]

Note: If your Swift version is 4.1 or greater than use compactMap(_:) because flatMap is deprecated in Swift 4.1.

let sortedArray = array.compactMap({ Double($0.replacingOccurrences(of: ",", with: ".")) }).sorted()

Swift 4.1 compactMap for lazy collections

Yes, absolutely the same. They just decided to remove the ambiguity of flatMap functions and renamed one of them to compactMap not to confuse developers.

Here is the proposal: Introduce Sequence.compactMap(_:)

We propose to deprecate the controversial version of a Sequence.flatMap method and provide the same functionality under a different, and potentially more descriptive, name.

Sample Image

What is the Swift compiler doing with my return type? Is it somehow casting?

No, there is no casting going on. There are simply two different flatMap functions being called. LazyMapSequence has two flatMap(_:) functions (well, technically four, but two are deprecated).

In your first code block, this function is inferred (because this version of flatMap has a return type that matches your allRegions function's return type):

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

And in your second code block, this function is inferred (because there is no type annotation on your local variable that's forcing it to choose the above version of flatMap):

func flatMap<SegmentOfResult>(_ transform: @escaping (Element) -> SegmentOfResult) -> LazySequence<FlattenSequence<LazyMapSequence<LazyMapSequence<Base, Element>, SegmentOfResult>>> where SegmentOfResult : Sequence

Swift array in array. Using compactMap / map instead of nested loops

I finally got it right:

if !activeDeals.isEmpty {

let bookings = activeDeals
.map { $0.bookings } // Gets the arrays of bookings
.compactMap { $0 } // Gets rid of the nils
.flatMap { $0 } // Flattens it

let carSelectionViewItem: [CarSelectionViewItem] = bookings.map {
let make = $0.vehicle?.make ?? ""
let model = $0.vehicle?.model ?? ""
let title = "\(make) \(model)"

return CarSelectionViewItem(icon: $0.vehicle?.carImage ?? "",
title: title,
description: String(format: "car_pending_booked_description".locale,
getNewDateStringFormat(string: $0.startDate)),
bookingStatus: .active,
dealStatus: .won,
dealId: $0.id ?? "",
showMoreFunc: {})
}

viewItems.append(contentsOf: carSelectionViewItem)
}


Related Topics



Leave a reply



Submit