combineLatest vs. zip in ReactiveCocoa

Article summary

Recently, I’ve been learning the ins and outs of Functional Reactive Programming with Reactive Cocoa. The library comes with a number of operators that can be used to modify incoming signals. In this post, I am going to compare two of those operators: +combineLatest: and +zip:.

Both +combineLatest: and +zip: can be used to combine two (or more) signals into one new signal that sends a single RACTuple containing both values. This can be very useful when two separate events occur, or when two pieces of changing data need to be processed together.

There are also +combineLatest:reduce: and +zip:reduce: versions of the operators. I’ll be using these variations in the examples below.

+combineLatest:

From the ReactiveCocoa Basic Operators documentation:

The +combineLatest: and +combineLatest:reduce: methods watch multiple signals for changes, and then send the latest values from all of them when a change occurs.

It is important to understand that the combined signal will not send a value until all of the inputs have sent their first value. Once all of the inputs have sent a value, the combined signal will send the latest value from each. This means that some values could be dropped if one of the input signals is sending changes before another input signal sends anything.

Once that “starting point” has been crossed and all of the input signals have sent a value then, each time any of the inputs sends a value, the combined signal will fire a tuple containing all of the latest values.

combineLatest

Here is an example (taken nearly verbatim from the ReactiveCocoa documentation mentioned above):

RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *combined = [RACSignal
    combineLatest:@[ letters, numbers ]
    reduce:^(NSString *letter, NSString *number) {
       return [letter stringByAppendingString:number];
    }];

// Outputs: B1 B2 C2 C3 D3 D4
[combined subscribeNext:^(id x) {
    NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[letters sendNext:@"B"];
[numbers sendNext:@"1"];
[numbers sendNext:@"2"];
[letters sendNext:@"C"];
[numbers sendNext:@"3"];
[letters sendNext:@"D"];
[numbers sendNext:@"4"];

Running this example will produce:

B1 B2 C2 C3 D3 D4

There a couple of interesting things to note here. First, “A” is never sent at all because the letters signal was changed to “B” before the numbers signal sent its first value. Second, the combined signal sends a new value as soon as either of the inputs change (once they both have sent an initial value). +combineLatest: sends as soon as either of the inputs change – it does not wait for both values to change.

+zip:

The +zip: operator combines two signals, sending the first value from each signal, then the second value from each signal, and so on. It doesn’t matter in which order the input signals are sent, the combined signal will wait to send until each input has sent its next value.

zip

This example is identical to the one above, but with +zip: replacing the +combineLatest: usage.

RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];

RACSignal *combined = [RACSignal
    zip:@[ letters, numbers ]
    reduce:^(NSString *letter, NSString *number) {
       return [letter stringByAppendingString:number];
    }];
                       
// Outputs: A1 B2 C3 D4
[combined subscribeNext:^(id x) {
    NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[letters sendNext:@"B"];
[numbers sendNext:@"1"];
[numbers sendNext:@"2"];
[letters sendNext:@"C"];
[numbers sendNext:@"3"];
[letters sendNext:@"D"];
[numbers sendNext:@"4"];

With +zip: the output is quite a bit different from what +combineLatest: gives you:

A1 B2 C3 D4

Unlike with +combineLatest:, the “A” is not dropped and the output is made up of the first pair from each input, followed by the second pair from each input, etc. Even though “A” and “B” are sent on letters before “1″ and “2″ are sent on numbers, they are still paired up in the order in which they are sent on their respective signals.

I found this easier to visualize using a Ruby code snippet in IRB:

letters = ["A", "B", "C", "D"]
numbers = ["1", "2", "3", "4"]
letters.zip numbers
=> [["A", "1"], ["B", "2"], ["C", "3"], ["D", "4"]]

Conclusion

When you need a signal that sends a value each time any of its inputs change, use +combineLatest:. When you need a signal that sends a value only when all of its inputs change, use +zip:.

Lastly, I think it would be useful to find a way to combine two signals and have the resulting signal only fire when both inputs change (like +zip:) but also send the latest values from each input signal (like +combineLatest:). Both input signals would have to fire again before the combined signal would fire. I’m not sure, but this might require making a custom operator. Leave a comment if this is something you’ve done with ReactiveCocoa.

Conversation
  • Joe Hosteny says:

    I think what you want is this:

    [RACSignal zip:@[ signalA, [signalB sample:signalA]]]

    See also https://github.com/ReactiveCocoa/ReactiveCocoa/issues/977 for a discussion.

  • Shiki says:

    Very clear explanation. Thanks!

  • Hung says:

    Thank you! It’s super clear explanation!

  • Nik Kov says:

    Very good, good lack

  • Michael says:

    One additional difference that can be important is that if you have two observable streams that will produce a different number of results…

    A => 1, 2, 3
    B => A, B, C, D

    – zip will stop firing (since it is waiting for stream A to produce another value).
    – combineLatest will fire 3D on the fourth tick.

  • Gerson says:

    You deserve a prize for that explanation! Thank you!

  • Comments are closed.