How do you read a pipeline, and how do combineLatest, merge, zip and scan differ?
Predict first
What do filter, map and scan produce over small ranges?
import Combine
var bag = Set<AnyCancellable>()
var out: [Int] = [], sc: [Int] = []
(1...6).publisher.filter { $0 % 2 == 0 }.map { $0 * 10 }.sink { out.append($0) }.store(in: &bag)
(1...4).publisher.scan(0, +).sink { sc.append($0) }.store(in: &bag)
print(out, sc)
- [20, 40, 60] [1, 3, 6, 10]
- [10, 30, 50] [10]
- [20, 40, 60] [10]
Predict first
Two subjects feed combineLatest. What is emitted?
import Combine
var bag = Set<AnyCancellable>()
let a = PassthroughSubject<Int, Never>(), b = PassthroughSubject<String, Never>()
var cl: [String] = []
a.combineLatest(b).sink { cl.append("\($0)\($1)") }.store(in: &bag)
a.send(1); a.send(2); b.send("x"); a.send(3)
print(cl)
- ["1x", "2x", "3x"]
- ["2x", "3x"]
- ["3x"]
Predict first
zip of three numbers and two letters:
import Combine
var bag = Set<AnyCancellable>()
var z: [String] = []
[1, 2, 3].publisher.zip(["a", "b"].publisher).sink { z.append("\($0)\($1)") }.store(in: &bag)
print(z)
- ["1a", "2b"]
- ["1a", "2b", "3b"]
- ["1a", "2b", "3"]
Mental model. merge is two roads joining into one lane. zip is dancing partners who must both arrive before they step. combineLatest is a scoreboard that updates whenever either team scores.
Read a pipeline from the source downward; each operator is a function from one publisher to another. Transform operators change values one at a time: map, filter, compactMap, removeDuplicates, scan (a running reduction that emits every intermediate result, so scan(0, +) over 1…4 gives 1, 3, 6, 10) and reduce (emits only the final value at completion). Combine operators merge several publishers and differ in when they emit and what they pair. merge interleaves values from same-typed publishers as they arrive. zip pairs the n-th value of each, waits for a partner, and finishes when the shortest finishes: [1,2,3] zipped with ["a","b"] gives (1,a) and (2,b). combineLatest emits a tuple of the latest value from each whenever any of them emits, but only after every input has produced at least one value: it is the operator for “form valid when email and password are both set”.
A few reading rules. Operators run synchronously on the thread that delivers the value, unless you move them with receive(on:). Errors skip every value operator and go straight to the subscriber, ending the pipeline. Never failure types let you use assign and sink without a completion handler. Types change down the chain, so use eraseToAnyPublisher() at API boundaries, and name intermediate publishers when a pipeline is long. Prefer a few well-known operators over clever ones; if a pipeline needs five nested flatMaps, an async function is probably clearer. Use print() (it logs every event) and handleEvents to debug.
Even numbers are kept and multiplied by ten, and scan emits the running total at each step.
Nothing is emitted until both sides have a value. When b sends "x", the latest a is 2, and then 3 pairs with "x" again.
zip pairs by position and stops when the shorter side ends, so the third number has no partner.
Common traps
- Expecting
combineLatestto emit before every input has a value. It stays silent until each one has produced something. - Confusing
zipwithcombineLatest.zippairs by position and waits;combineLatestreuses the latest value of the other side.
They might follow up with
- When do you use
scaninstead ofreduce? When you want every intermediate result, not only the final one. - How do you debug a pipeline?
print("tag")orhandleEventsat each stage.
Related questions
- 11.2.2 How do flatMap and switchToLatest differ, and when do you use each?
- 11.2.3 What do debounce, throttle and removeDuplicates do, and what do share and multicast change?
- 11.1.1 What are Publisher, Subscriber, Subscription and Subscriber.Input/Failure, and when does a pipeline actually…
How do flatMap and switchToLatest differ, and when do you use each? →