RAS S1E1

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:

  1. It assumes the parser will never fail.
  2. 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:

  1. Do the parsing in a different scheduler.
  2. Provide a default value in case the parser fails.
  3. 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.