RAS S1E4

Flatmap is one of those ubiquitous operators that, independently of your expertise level, will be present in your day-to-day work with ReactiveSwift. In this post I will talk about how we can leverage its strategies with an example.

If you are not familiar with them:

  1. latest
  2. race
  3. merge
  4. concact
  5. concurrent1
Example

We will use this scenario throughout the post when describing the strategies and reason about which one would make sense.

  1. We are observing the user typing text. 📝
  2. Every time she hits Enter (new line), we pick up the entire paragraph and translate it. 2
  3. Because translation might take a while and a paragraph might be really big, we will translate a single sentence at the time and send it back.
  4. For simplicity sake, we will assume that this service won't throw any error. 😇

This could look like this:

let text: Signal<String, NoError> = ...  
let translate: (String) -> Signal<TranslatedSentence, NoError> = ...  

And TranslatedSentence:

struct TranslateSentence {  
   let originalText: String
   let translatedText: String
   let language: String
}

Finally:

let strategy: FlattenStrategy = ⁉⁉⁉⁉  
let translatedText = text.flatMap(strategy, translate)  

Let's try to replace that ⁉⁉⁉⁉.

1. .latest

Forward only values from the latest inner stream sent by the stream of streams.

The active inner stream is disposed of as a new inner stream is received.

The flattened stream of values completes only when the stream of streams, and all the inner streams it sent, have completed.

Any interruption of inner streams is treated as completion, and does not interrupt the flattened stream of values.

Any failure from the inner streams is propagated immediately to the flattened stream of values.

With this strategy applied to our example, every time a new paragraph arrives, while translating the previous one, we will simply dispose the latter and start translating the former.

If the user is a fast typer and the translation takes a while this will happen. 😖

Also remember that paragraphs can have different sizes, so the first one could be huge and the second quite small. ❌

2. .race (aka amb)

Forward only events from the first inner stream that sends an event. Any other in-flight inner streams is disposed of when the winning inner stream is determined.

The flattened stream of values completes only when the stream of streams, and the winning inner stream, have completed.

Any interruption of inner streams is propagated immediately to the flattened stream of values.

Any failure from the inner streams is propagated immediately to the flattened stream of values.

This one is pretty easy: the first translated sentence wins! 🏎 🤣

What this means is that if the second paragraph sends a translated sentence before the first one, it will dispose of the first paragraph and only send translated sentences from the second one.

In our case, this probably wouldn't be the right strategy. ❌

3. .merge

The stream of streams is merged, so that any value sent by any of the inner streams is forwarded immediately to the flattened stream of values.

The flattened stream of values completes only when the stream of streams, and all the inner streams it sent, have completed.

Any interruption of inner streams is treated as completion, and does not interrupt the flattened stream of values.

Any failure from the inner streams is propagated immediately to the flattened stream of values.

With .merge we won't dispose of anything, but we might run into a situation where the sentences get mixed up, which might yield some funny results, but it's probably not what we would want. 😅

Remember although we are only starting the translation when we get a full paragraph, we are sending back one single translated sentence at the time. We could run into a situation where the first sentence of the second paragraph gets processed before the last sentence of the first paragraph. ❌

4. .concat

The stream of streams is concatenated, so that only values from one inner stream are forwarded at a time, in the order the inner streams are received.

In other words, if an inner stream is received when a previous inner stream has yet terminated, the received stream would be enqueued.

The flattened stream of values completes only when the stream of streams, and all the inner streams it sent, have completed.

Any interruption of inner streams is treated as completion, and does not interrupt the flattened stream of values.

Any failure from the inner streams is propagated immediately to the flattened stream of values.

Finally .contact. In this strategy, each received paragraph would be enqueued (FIFO) and processed. The second paragraph would only start once the first has terminated and so forth.

This seems to be exactly what we want, since it respects the order in which each paragraph arrives! ✅

Conclusion

ReactiveSwift offers a powerful toolkit when it comes to how we should compose our operations. In this case .concat is an obvious choice, in contrast with .race. But imagine a scenario where you just want to display some data to the user, so you try your local cache and the network, the first one that arrives with results is the one that you use, this would be a good situation to use .race! 🏎

Any question or suggestion, leave a comment or hit me up on twitter!

  1. I won't talk about concurrent in this post. In any case, both merge and concat use it internally (merge is a concurrent with Int.max limit and contact with 1 as the limit).

  2. Internally we could have a buffer that would keep growing while the user was typing and finally make the translation of each sentence. (of course these are implementation details and unrelated to this post)🤓