Note: this will be a series of posts about ReactiveSwift.
When explaining ReactiveSwift, I tend to go away from FRP entirely and linger into familiar concepts, like the Optional
type. The problem, is that the later doesn't provide all the necessary mental framework needed to explain certain peculiarities, so it becomes an exercise in frustration. Not only that, Optional
falls short in two other concepts:
Error
information is discarded: there is either a.some(T)
or.none
- It's synchronous:
SignalProducer
/Signal
are intrinsically asynchronous.
This becomes quite obvious when explaining the usage of map
versus flatMap
in a ReactiveSwift context.
In simple terms, the difference is the return type. While a map
would return you a U
, a flatMap
would give you a m U
. In this case m
being a SignalProducer
. So where is this useful?
You could imagine a parsing function to have the following type T -> U
(e.g. Data -> [Person]
):
let parsePersons1: (Data) -> [Person] = ...
let request: URLRequest = ...
let session: URLSession = ...
let fetchPersonsFlow: SignalProducer<[Person], MyError> = session.reactive.data(with: request)
.map(parsePersons1)
With a flatMap
:
let request: URLRequest = ...
let session: URLSession = ...
let parsePersons2: (Data) -> SignalProducer<[Person], MyError> = { data in
return SignalProducer { observable, disposable in
observable.send(value: parsePersons1(data))
observable.sendCompleted()
}
}
let fetchPersonsFlow: SignalProducer<[Person], MyError> = session.reactive.data(with: request)
.flatMap(strategy: .latest, transformation: parsePersons2)
Note: RAS has the concept of strategy when working with a
flatMap
. This is beyond this post.
They do seem quite similar, but the map
approach defers in two fundamental point:
- It assumes the parser will never fail.
- It doesn't allow to further compose the parsing.
The first point is fairly straightforward, since we are "promising" a [Person]
as return type.
The second point is more elusive and it takes a bit of time to get used to it, but it opens the door to things like:
- Do the parsing in a different scheduler.
- Provide a default value in case the parser fails.
- Inject other side effects (e.g. logging).
Different Scheduler
let fetchPersonsFlow: SignalProducer<[Person], MyError> = session.reactive.data(with: request)
.flatMap(strategy: .latest) { data in
parsePersons2(data).start(on: QueueScheduler(name: "Parsing.Queue"))
}
Default value
let defaultValue: SignalProducer<[Person], MyError> = ...
let fetchPersonsFlow: SignalProducer<[Person], MyError> = session.reactive.data(with: request)
.flatMap(strategy: .latest) { data in
parsePersons2(data).flatMapError { error in defaultValue }
}
Logging
let fetchPersonsFlow: SignalProducer<[Person], MyError> = session.reactive.data(with: request)
.flatMap(strategy: .latest) { data in
parsePersons2(data).logEvents()
}
It's important to note that (example1
):
let fetchPersonsFlow: SignalProducer<[Person], MyError> = session.reactive.data(with: request)
.flatMap(strategy: .latest) { data in
parsePersons2(data).flatMapError { error in defaultValue }
}
Is fundamentally different than (example2
):
let fetchPersonsFlow: SignalProducer<[Person], MyError> = session.reactive.data(with: request)
.flatMap(strategy: .latest, transformation: parsePersons2)
.flatMapError { error in defaultValue }
In the first case, we provide a defaultValue
if the parsePersons2
fails. In the second case, we provide a defaultValue
when either the session.reactive.data
, or the parsePersons2
, fails. example1
gives us flexibility to further extend it:
let defaultValueWhenNetworkFails: SignalProducer<[Person], MyError> = ...
let fetchPersonsFlow: SignalProducer<[Person], MyError> = session.reactive.data(with: request)
.flatMap(strategy: .latest) { data in
parsePersons2(data).flatMapError { error in defaultValue }
}
.flatMapError { error in defaultValueWhenNetworkFails }
We are now sure that if something fails, it will be because of session.reactive.data(with: request)
, so we can safely provide a defaultValueWhenNetworkFails
that is related to it.
This is a silly example, but this flexibility becomes vital when dealing with complex business requirements:
To conclude:
Use
map
when you are dealing with a pure transformation.