An inclusive overload of Publisher.prefix(while:)

(Assumed audience: folks familiar with Combine.)

Combine ships with handful of prefixing operators:

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.

(Gist permalink.)

First, the case where value passes predicate,

(Gist permalink.)

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.

(Gist permalink.)

Now, rewriting the prefixInclusive(while:) scaffolding we were working under.

(Gist permalink.)

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.

(Gist permalink.)

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).

(Gist permalink.)

And finally, giving it a spin with the isEven predicate.

(Gist permalink.)

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:).

(Gist permalink.)

⇒ 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).