Recently, the Functional Reactive Programming (FPR) paradigm has been gaining momentum, but getting started can be hard as its necessary to change the way you’re thinking about the datafow in your program.
For some time now I’ve been answering questions on StackOverflow regarding ReactiveCocoa.
From time to time there are reasonable and seemingly simple questions (with code samples) where there’s a simple answer to the questions and then theres a more elaborated answer of the kind “If you restructure your code completely it could be much simpler”.
I want to take a look at some of these questions in more detail.
The question was posted in ObjC using ReactiveObjC. I’ll adapt the code here in Swift using ReactiveSwift and SignalProducer
for RACSignal
as the signals in the question have cold semantics (See here if you are not familiar with hot vs. cold semantic).
At first sight, this questions has a very simple answer. Given you have some producers, use the merge
operator and the resulting producer will complete
will only complete when all merged producers have completed.
let producerA: SignalProducer<Any, NoError>
let producerB: SignalProducer<Any, NoError>
let both = SignalProducer.merge([producerA, producerB])
Here, both producers will be started immediately and if their values are produced after some delay, the values might be interleaved. If it is necessary for the to be started in order, use concat
instead of merge
. (In ReactiveSwift, you’ll have to do let both = SignalProducer([producerA, producerB]).flatten(.concat)
).
Looking closer at the code sample in the question, you’ll find that the inner producers are not independent from each other. There is an implicit dependency via the contactModelMutableArray
array:
var contactModel = []
let selectMessages = SignalProducer {
Database.loadMessages { modelArray in
// Populate `contactModel` with contacts from database
for(model in modelArray) {
if(model.uid > 0) {
contactModel.append(model)
}
}
}
}
let selectInfo = SignalProducer {
// Iterate through `contactModel` and update elements
for model in contactModel {
// Request information for model and update the model
// Note: self.loadInformation returns a Producer
self.loadInformation(for: model).start { data in
model.data = data
}
}
}
let replace = SignalProducer {
// Iterate through `contactModel` and update/replace database row for element
for model in contactModel {
Database.update(model)
}
}
This approach is actually not that surprising if the asking person is not yet completely comfortable with FPR and is still mixing in imperative patterns. Some other issues are:
Database.loadMessages
already returned a SignalProducer, it not necessary to wrap this in another SignalProduceruid
can be done with filter
self.loadInformation
might be asynchronous, in that case there’s no guarantee that the model is already updated by the time it is written back to the databaseThe most important issue is the implicit data flow via the shared contactModel
array. On a high level, the data flow is pretty straight forward
So lets implement this dataflow explicitely, with each step handing its data on to the next step.
For the first step, we’ll update selectMessages
:
let selectMessages = Database.loadMessages
.flatMap { SignalProducer(values: $0) }
.filter { $0.uid > 0 }
Database.loadMessages
sends an array of models as value (SignalProducer<[Model]>
). Because we want to process each single model in the following steps, it would be easier if we had a producer that sent each of the models as a separate event (SignalProducer<Model>
). We can achieve this by using flatMap
and the convenience initializer SignalProducer.init(values:)
which takes a Sequence
and sends each element of the sequence as value and the completes. We need to use flatMap
, because with only map
we would get a SignalProducer<SignalProducer<Model>>
and the flatMap
basically “flattens” that to SignalProducer<Model>
. Now, we can use filter
to check the uid
s.
For the second step, we want to process each Model
that is sent and load information for that model:
let selectInfo = selectMessages
.flatMap { self.loadInformation(for: $0) }
Since we’re sending the models as values on the signal, this is now very simple. The only thing to keep in mind is that we have to use flatMap
here again to “flatten” the stream just like before.
The last step is basically the same as the second one:
let replace = selectInfo
.flatMap { Database.update($0) }
Take all these together and the solution looks like this:
let selectMessages = Database.loadMessages
.flatMap { SignalProducer(values: $0) }
.filter { $0.uid > 0 }
.flatMap { self.loadInformation(for: $0) }
.flatMap { Database.update($0) }
Now that the data “flows” through these operators and the dependencies between them is explicit, there suddenly are not 3 (seemingly) independent signals anymore but only one. And the initial Question “how to wait until all signals complete” does not even arise anymore. Oh, and furthermore, this is a lot less code than before.
I have prepared a Playground on github with a working sample of the proposed solution.
Markus is a technical mastermind and one of the founders of Innovaptor. He studied Computer Engineering at Vienna University of Technology and contributes a valuable diversification to Innovaptor's qualifications. Markus is eager to find innovative solutions to complex problems in order to provide products that are tailored directly to the customers' needs