Modifying the pairwise operator with a duplicated start11 Feb 2021 ⇐ Notes archive
(This is an entry in my technical diary. There will likely be typos, mistakes, or wider logical leaps — the intent here is to “[let] others look over my shoulder while I figure things out.”)
I need to compare changes to values within a sequence, they’re non-optional already, so I need a way to see the previous value and the current value. I could probably figure that part out, but what about the first pair? I can’t wait until I get two values and I’d rather not introduce optionals, but what if I duplicated the first value that came down the stream but otherwise always had the previous and current values?
pairwise — which is an overlay on
nwise(2) — will only send values downstream after two are received from its upstream. So, it doesn’t meet the “I can’t wait until I get two values” requirement.
And further, the “rather not introduce optionals” mention makes things trickier. Let’s take a look. An initial approach we could take is
shareing upstream, spitting it off into two — one left as is and another that’s
first’d — and then connecting the split sequences with a
flatMap that duplicates the first value and then finally
pairwiseing the result (…that probably made zero sense in prose, here’s a sketch hah).
(1, 1) surprised me — I had assumed
shareing upstream would ensure that the resubscribe in the
flatMap would carry on with
2. I scratched my head and dug further into this bit.
The second subscription to
upstream in the
flatMap seems to synchronously win out over the lingering
2 from the first subscription (notice it comes in at the bottom of the logs, yet never makes it to the
delaying the first value event and then noticing the second subscription missed its chance entirely.
Stepping back, there miiight be a way to make a
share based approach without race conditions like this while also accounting for
upstream’s temperature (please reach out, if you know how (!)), but at this point, I decided to ease the “rather not introduce optionals” requirement and use a
scan-based approach to work around these concerns.
The core of the implementation is at
(1) — to kick off the scan, we coalesce the initial
(nil, nil) to
(next, next) for the first upstream value. From then on, we pair à la
pairwise since subsequent calls to
optionalZip will have a non-
nil return value. It’s a bit unfortunate that the local, free-function
optionalZip couldn’t simply be named
zip, since that would collide with
Publisher.zip in the implementation. Adam mentioned he runs into this often with
Publisher.print, too. Wonder if there’s any reason Swift can’t have an inverse to the
@_disfavoredOverload annotation — maybe called
@_preferredOverload — to nudge the compiler here? We could prefix with the current module’s name in most cases, but I was working within a Playground (and I’m guessing they have generated module names that are out of reach at compile time).