Map and flatMap difference in optional unwrapping in Swift 1.2
Asked Answered
G

4

6

Both map and flatMap are defind on ImplicitlyUnwrappedOptional, but they differ (obviously) in their definition according to the documentation:

func map(f: @noescape (T) -> U) -> U!

If self == nil, returns nil. Otherwise, returns f(self!).

func flatMap(f: @noescape (T) -> U!) -> U!

Returns f(self)! iff self and f(self) are not nil.

I tried using them with a simple example:

let number: Int? = 1

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

res1 //3
res2 //3

But they produced the same results even if number was nil. So my question is, what is the actual difference between them if I apply map or flatMap to ImplicitlyUnwrappedOptionals? Which one should I choose over the other and when?

Graphy answered 10/4, 2015 at 8:23 Comment(0)
P
10

(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.

Preciousprecipice answered 10/4, 2015 at 8:57 Comment(3)
It's true, though, that the distinction between map and flatMap can be difficult to discern because they are not strict about the closure type they take. In many cases they can take the same closure and produce the same result. Maybe flatMap should balk if the closure doesn't produce an Optional.Latinity
Optional.map() can return nil because U is a generic type, which can be whatever you want, including an optional.Mesolithic
@PeterSchorn: Yes, as soon as nested optionals are involved nil can mean different things. I have tried to rewrite the answer in order to avoid that ambiguity. Of course U can be an optional type as well.Preciousprecipice
L
8

This is not possible with map() where the mapping closure has the signature (T) -> U.

That's not quite right. In my view, Martin R's answer doesn't quite get at the heart of the problem, which is that the docs do not correctly describe the difference between map and flatMap.

The difference is not what kind of closure they take. Each will happily accept a closure that produces a nonOptional or a closure that produces an Optional — despite what the docs say, and despite the difference in their declarations. All of these expressions compile:

let i : Int? = nil
let result1 = i.map {_ in "hello"} // map, closure produces nonOptional
let result2 = i.flatMap {_ in "hello"} // flatMap, closure produces nonOptional
let result3 = i.map {_ in Optional("hello") } // map, closure produces Optional
let result4 = i.flatMap {_ in Optional("hello") } // flatMap, closure produces Optional


Okay, so what's the actual difference? It's what flatMap does just in case the closure does produce an Optional: it unwraps it, thus preventing a double-wrapped Optional:

let i : Int? = nil
let result1 = i.map {_ in "hello"} // String?
let result2 = i.flatMap {_ in "hello"} // String?
let result3 = i.map {_ in Optional("hello") } // String?? // double-wrapped
let result4 = i.flatMap {_ in Optional("hello") } // String? // not double-wrapped

That is the only difference between map and flatMap.

Latinity answered 16/6, 2018 at 15:53 Comment(2)
I interpret it a bit differently. flatMap takes a closure of type (T) -> U?, i.e. the closure always returns an optional. In the second example, the closure {_ in "hello"} is converted by the compiler to {_ in Optional("hello") }, so that it is the same as the fourth example. In both cases, Optional("hello") is returned, there is no unwrapping.Preciousprecipice
Yes, flatMap doesn't actually unwrap the optional returned by the closure; the difference is that it doesn't wrap result of the closure in another optional, whereas map does. This is the fundamental difference between the methods that your answer didn't address.Mesolithic
A
0

flatMap resolves nested optionals whereas map does not.

flatmap

var temp: Int? = 3
var flag: Bool = false
print(temp.flatMap { $0 < 5 ? 1 : nil } ?? .zero) 

// output: 1

map

var temp: Int? = 3
var flag: Bool = false
print(temp.map { $0 < 5 ? 1 : nil } ?? .zero) 

// output: Optional(Optional(1))
Archilochus answered 10/2, 2021 at 2:2 Comment(0)
L
0

[map vs compactMap vs flatMap]

Swift Optional map vs flatMap

Let's create our own simple realisation of Optional type and implement map and flatMap functions

enum CustomOptional<T> {
    case none
    case some(T)
    
    public init(_ some: T) {
        self = .some(some)
    }
    
    func map<U>(_ transform: (T) -> U) -> CustomOptional<U> {
        switch self {
        case .some(let value):
            let transformResult: U = transform(value)
            let result: CustomOptional<U> = CustomOptional<U>(transformResult) //<-- force wrap the transformResult
            return result
        case .none:
            return .none
        }
    }
    
    func flatMap<U>(_ transform: (T) -> CustomOptional<U>) -> CustomOptional<U> {
        switch self {
        case .some(let value):
            let transformResult: CustomOptional<U> = transform(value)
            let result: CustomOptional<U> = transformResult
            return result
        case .none:
            return .none
        }
    }
}
  • map - can return Optional Optional
  • flatMap - can flat Optional Optional to Optional
Optional.map { () -> T } -> Optional<T>
Optional.map { () -> Optional<T> } -> Optional<Optional<T>>

Optional.flatMap { () -> Optional<T> } -> Optional<T>

map: returned value of transformed function is wrapped into Optional of returned value of map function

flatMap: returned value of transformed function is the same as returned value of flatMap function

//problem

//T == Int, U == CustomOptional<String>
//map<U>(_ transform: (T) -> U) -> CustomOptional<U>
//map<CustomOptional<String>>(_ transform: (Int) -> CustomOptional<String>) -> CustomOptional<CustomOptional<String>>
let result: CustomOptional<CustomOptional<String>> = CustomOptional(1).map { int in
    return CustomOptional("Hello: \(int)")
}

//solution

//T == Int, U == String
//flatMap<U>(_ transform: (T) -> CustomOptional<U>) -> CustomOptional<U>
//flatMap<U>(_ transform: (Int) -> CustomOptional<String>) -> CustomOptional<String>
let result5: CustomOptional<String> = CustomOptional(1).flatMap { int in
    return CustomOptional("Hello: \(int)")
}

[Swift Functor, Applicative, Monad]

Layton answered 6/11, 2021 at 20:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.