A materialization primer09 Apr 2020
(Assumed audience: folks with a working knowledge of Combine and the
When I first encountered the word, I thought it was over the top. And, it kind of is. This entry tries to bring it down from abstraction and into Swift.
Let’s run into needing it by way of example, fetching integers from random.org.
Fetching random integers
We’ll extend Point-Free’s pinging of random.org for a random number.
randomNumberPublisher in hand, we can imagine a view model using it when responding to pings to fetch a number.
randomNumberPing a few times and see what happens.
And while not ideal, it checks out that a failure in the first request would preclude attempting the second. The
Publisher contract states that an error event means a publisher won’t further emit values, effectively rendering our
viewModel inert after a failure.
It might be tempting to rework
RandomNumberViewModel.init with error handling.
Then, well, we run into another problem. We have no way of disambiguating between an initial and error state, since they both bear
nil values. Moreover, if we had more errors than
RandomDotOrgError.failedRequest, we’d lossily squash them.
We could lift
RandomNumberViewModel.randomNumber into a
Result, but, doing so would put the burden on consumers to
.failure when focusing in on the cases they’re concerned about (e.g. content views, the success case, and error views, the failure).
Materialization—as you probably guessed—has a say in this matter.
First, let’s upgrade our view model output from an
Int? to a type that better relays error and loading states.
And then, trying to wire everything up with
Let’s take a breather. That was a ton of scaffolding to climb through, but, it’ll pay off—I promise!
(10) are gaps we’ll fill in with materialization.
<aliens-guy>Errors as values</aliens-guy>
We’ll often need publishers to drive client-side errors views—which, seem at odds with Combine’s
[After sending a subscription to a subscriber and they request values,] the publisher is free to send that number of values or fewer to the subscriber.
…, if the publisher is finite, then it will eventually send a [single] completion or an error.
Receiving only a single failure (or finished) event makes user-initiated retries and displaying error views difficult. We’d have to peek into the
receiveCompletion argument of either
.sink and then re-attach subscribers to new publisher instances.
This not-ideal’ness stems from completion events being terminal. There’s sound practical (and theoretical1) reasons for this and we’ll need to bridge the gap by moving our involved
Failure generic into its
“Into” in that sentence is a bit vague. Let’s pause with it.
Publishers emit either some (or none)
Output-typed values or a single (or no)
Subscribers.Completion<Failure> terminating event.
So, we need a way of translating a publisher to one in this form (subbing in
AnyPublisher as a placeholder),
AnyPublisher<Output, Failure> ⇒ AnyPublisher<Event<Output, Failure>, Never>.
It’s worth noting our old friend2,
Never prevents the transformed publisher from error’ing out.
I’ll also need to properly introduce our new friend,
Event. It’s an enumeration to represent the possible events a subscriber may receive.
This is all sounds nice and well. But, what’s the point?
By materializing errors into a publisher’s output, we can then react to them in the same way we would handle ordinary values.
Or, rather, we’re considering errors as values.
We have two options: lean on existing operators or sketch out our own
Publisher conformance. There’s tradeoffs for each and since there’s prior art for the latter in CombineCommunity/CombineExt, we’ll go with the former.
Now, we can start chaining operators, case by
Values? We need to box ‘em into
Completion.finisheds? Well, we need a way of silencing upstream’s finished event and then turning around and publishing an
Event.completion(.finished). Thankfully, the framework ships with a few
And lastly, (and densely3), catching error events.
That’s a lot to digest—let’s recap (and maybe take some Tums).
We’re doing three things.
- Lifting emitted values from upstream into the
- Intercepting completions with the
appendoperator and turning around and sending an
- Catching any
errors, boxing them up into a
Event.completion(.failure(error)), and pass it along as a value event.
Materializing—in a sort of pseudo-Swift syntax—takes a
Publisher<Output, Failure> and lifts it to a
Publisher<Event<Output, Failure>, Never>, shifting errors to the value side.
However, consumers might only be concerned with specific
Event cases. Think content views backed by
Output instances or error views driven by
We need a way of lowering a previously-materialized sequence, or as our wise elders called it, “dematerializing”
To start, we might be tempted to extend
Publisher with an
Output == Event<Output, Failure> constraint.
Sadly, the compiler can’t reconcile the recursive constraint. We’ll need to introduce an intermediary protocol,
EventConvertible, following Rx’s lead.
errors we’ll wanna focus in on upstream
.completion(.failure(_))s, respectively. There’s a couple of ways we could go about this—e.g. filtering cases or compacting associated values à la Point-Free’s enumeration properties.
The latter route is more fun—and of course, that doesn’t make the former any less valid.
To start, let’s add computed properties to
EventConvertible to pluck out either an associated
These optional properties tee up
compactMap them out.
“All together now (materializing and dematerializing).” — The Beatles…I think.
The above snippets should all compile, again, and now we can sketch out a
RandomNumberViewModel with a button to fire off requests, honor loading states, and present a sheet with either a random number or error in under thirty lines.
In GIF action,
You’re probably—and rightfully—thinking that performing the
errors dance on every
ObservableObject conformance is going to be…a lot.
The community agrees. Thankfully, we can stand on their shoulders. In the past, folks have abstracted away materialization under a higher-level type called
It comes in a few flavorings:
Each exposes observables and signals (synonyms for “publishers”) like
.executing corresponding to
…is there a Combine
Special thanks to Van for feedback on an early draft of this entry.
Related reading and footnotes
⇒ Shai’s implementation of materialization.
⇒ The ReactiveX reference for materialize and dematerialize.