iosswiftreactive-swift

Differences among flatmap strategies in ReactiveSwift


From the documentation of reactive swift I could understand about Flattening. Examples of which can be found here. In the section Flattening event streams everything has been discussed perfectly.

I'm confused about flatmap. According to the docs, it Maps each event from self to a new producer, then flattens the resulting producers according to strategy - concat/merge/latest. So, it should be similar to flattening I guess.

But, I could not generate a similar behavior. For example consider the following code segment. If I change the flattening strategy like concat/merge/latest, the output does not change.

let (numbersSignal, numbersObserver) = Signal<String, NoError>.pipe()
    
numbersSignal.producer.flatMap(.concat) { value -> SignalProducer<String, NoError> in
            print("Outer Signal Producer \(value)")
            return SignalProducer<String, NoError> {
                observer, lifetime in
                print("Inner Signal Producer \(value)")
                observer.send(value: "Inner")
                observer.sendCompleted()
            }
        }.observe(on: UIScheduler()).startWithValues { result in
           print("Observer \(result)")
        }


    numbersObserver.send(value: "A")
    numbersObserver.send(value: "B")
    numbersObserver.send(value: "C")
    numbersObserver.sendCompleted()

Output:

Outer Signal Producer A
Inner Signal Producer A
Observer Inner
Outer Signal Producer B
Inner Signal Producer B
Observer Inner
Outer Signal Producer C
Inner Signal Producer C
Observer Inner

Can anyone clear this up?

Moreover, can any examples be provided about flatmap differentiating the effect of merge, concat, latest?


Solution

  • What's happening here is that the producers you're creating in the flatMap complete synchronously; you're calling sendCompleted within the start closure itself. So within flatMap, it's calling start on the producer and the producer is finishing before that call to start even returns. This means there's no opportunity for flatMap to apply different strategies; each producer finishes immediately after it is started.

    We can see how the different strategies behave by creating an asynchronous producer within flatMap (note I'm using the most recent versions of Swift and ReactiveSwift, so I'm using Never instead of NoError):

    let (numbersSignal, numbersObserver) = Signal<TimeInterval, Never>.pipe()
    
    numbersSignal.producer
        .flatMap(.concat) { value -> SignalProducer<TimeInterval, Never> in
            print("Outer Signal Producer \(value)")
            return SignalProducer(value: value).delay(value, on: QueueScheduler())
        }
        .startWithValues { result in
            print("Observer \(result)")
        }
    
    
    numbersObserver.send(value: 5)
    numbersObserver.send(value: 2)
    numbersObserver.send(value: 1)
    

    In this example we're sending TimeInterval values, and each created producer sends the given value with a delay equal to that value.

    concat: Each subsequent producer waits for the previous one to complete before starting, so the values are printed in the exact order we send them.

    Outer Signal Producer 5.0
    Outer Signal Producer 2.0
    Outer Signal Producer 1.0
    Observer 5.0
    Observer 2.0
    Observer 1.0
    

    merge: All producers are started immediately once we send values, so they all run concurrently. Therefore, the values are printed smallest to largest i.e. the producers with the shortest delays complete first.

    Outer Signal Producer 5.0
    Outer Signal Producer 2.0
    Outer Signal Producer 1.0
    Observer 1.0
    Observer 2.0
    Observer 5.0
    

    latest: Only the last value is printed because each producer is cancelled as a new value comes in; only the last value is allowed to run to completion.

    Outer Signal Producer 5.0
    Outer Signal Producer 2.0
    Outer Signal Producer 1.0
    Observer 1.0
    

    Note that in all three cases, the "Outer Signal Producer" message was printed first for all values. This is because the closure we give to flatMap is always run immediately when a new value comes in. But the resulting producers are started and cancelled in accordance with the flatten strategy.