An inclusive overload of Publisher.prefix(while:)
17 Jan 2021(Assumed audience: folks familiar with Combine.)
Combine ships with handful of prefixing operators:
Publisher.prefix(_:)
for a bounded number of elements,.prefix(while:)
— and athrow
ing variant — for consecutive elements passing the providedwhile
predicate,.prefix(untilOutputFrom:)
for prefixing until another sequence publishes an element (which ended up being the backbone forCombineExt.Publisher.prefix(duration:)
).
We’re going to focus in on the second overload: prefixing along a predicate.
For the uninitiated, “predicate” is five-dollar speak for the less-abstract idea of assigning true
or false
to some value. That is, a function from a type A
to Bool
.
A predicate to check for even integers? let isEven = { $0 % 2 == 0 }
.
A predicate to filter in on administrator User
models? let admins = users.filter(\.isAdmin)
.
Yet, when it comes to prefixing, there are instances where we want to include the first element that didn’t pass the predicate. Think a publisher of valid game moves that are prefixed until an invalid one is made and rendered back to the player. Or intermediary steps of an algorithm being published, followed by completing with a goal state.
To modify Combine’s Publisher.prefix(while:)
method to include the first predicate-failing element, we’ll need to listen in on upstream values, pass through those the predicate succeeds on, and once we find an element that fails, project it downstream before a completion event.
“Listen in on upstream values,” “pass through,” and “project downstream” are all hints at a flatMap
— let’s start there.
First, the case where value
passes predicate
,
The else
clause is trickier. We need to project value
downstream , while also signaling upstream to complete successfully — maybe a small wrapper enumeration can encode this case and when predicate(value)
passes.
Now, rewriting the prefixInclusive(while:)
scaffolding we were working under.
To get things in compiling order, we’ll need to chain a couple of operators after the flatMap
call to pass through .isPredicatePassingValueOrIncluded
events and complete after an .end
comes through.
Now tidying up with type inference’s help (the implementation is dense, so no frets if it takes a few passes for it to click).
And finally, giving it a spin with the isEven
predicate.
Phew. That was a lot. If you’d like to fold this prefix overload into your project, I PR’d it to CombineExt with the ability to switch between exclusive and inclusive prefixing (!), taking from RxSwift.TakeBahavior’s prior art.
An alternate implementation
Joe Fabisevich asked in a DM:
And I take it there aren’t other ways to do inclusive prefixing like making a subsequence of the predicate-failing elements and taking the first value?
Turns out…we can! But, it requires a CombineExt
import to make use of Publisher.share(replay:)
.
■
Related links
⇒ Rob Mayoff’s answer to “How to make Combine’s prefix
operator inclusive?”
⇒ My initial implementation of CombineExt.ReplaySubject
wasn’t quite thread-safe — here’s a PR to fix that (following Maksym Shcheglov’s lead).