setFailureType, setOutputType, and initial objects
06 Apr 2020(Assumed audience: folks familiar with Combine.)
Publisher.setFailureType(to:)
is a head scratcher. At a glance, it seems to step on .mapError(_:)
’s toes. And, it sort of does!
The “available when Failure
is Never
” note in the documentation hints at why.
So, let’s spin up our favorite Never
-erroring publisher, Just
, and map over its error type for a closer look.
Hmm. We need to implement (Never) -> OurError
, eh? Maybe we can signal an OurError
return, ignore the input, and call it a day?
This compiles‽
If you scratched your head at this, don’t worry. I did, too.
That’s the magic setFailureType
generalizes and tucks away. In fact, we can prove that somewhere in Combine’s closed source, Apple calls { _ -> NewFailure in }
for some generic failure type, NewFailure
.
Before we do, it’s natural to ask if Publisher
’s other associated type, Output
, has the same affordance.
Whelp—time to write our own (!).
Publisher.setOutputType(to:)
After learning about setOutputType
from Adam and PR’ing it to CombineExt
, a couple of folks asked about its value.
[…], if the upstream is
Never
’d you’d probablyappend
another publisher, which would change the output type to my understanding.
This is almost the case! Let’s dig further.
A way to Never
out a publisher is ignoring its output. Doing so turns it into a “completable” that only notifies downstream of Subscribers.Completion<Failure>.finished
or .failure
events.
We did this often at Peloton.
Our API had a notion of “workout finalization,” after which, a workout’s metrics would be min-max’d, summed, averaged, and cleaned before reporting secondary statistics like achievements and streaks.
So, we’d ignore the output of the publisher backing the finalization request and then, if successful, concatenate another publisher—of a different output type—to fetch any finalization-dependent metadata.
Some issues shake out when mirroring this in the above gist by append
ing another element.
Downstream from the third line, Output
is fixed to Never
, forcing the append
method to accept either a variadic number of Never
s, a Sequence
of ‘em, or another Never
-outputting publisher—each corresponding to the three available overloads:
Publisher.append(_:)
(variadic)..append(_:)
(Sequence
)..append(_:)
(Publisher
).
The first isn’t possible by extension of Never
being uninhibited, the second only works with an empty Never
sequence, and the third would mean chaining another “completable” (which, does have its uses).
We need to map Never
into another type and that’s where setOutputType
fits.
To start, let’s scope the implementation by providing a (Never) -> String
.
The { _ -> String in }
closure is a vacuous transformation. In that, since it’s never called, we can claim to return any type.
Requiring folks to roll their own { _ -> T in }
closures for each T
isn’t ideal and that’s the cosmetic setOutputType(to:)
and setFailureType(to:)
provide.
I should address the rogue type erasure after the Just
publisher.
Turns out Apple buried some Easterlockdown eggs there—removing the erasure and setOutputType
call still compiles.
Combine ships with overloads of ignoreOutput
for specific publishers to keep Output
in tact, but that isn’t true across all publishers (and why I reached for erasure to demonstrate the subtlety).
setFailureType(to:)
and initial objects
You might’ve noticed—it’s okay, if you didn’t—that setOutputType
’s implementation is kind of “forced” by the compiler to a lone map
call (sans possibly sprinkling print
s, fatalError
s, or other statements around).
If we tried to implement our own setFailureType
operator, we’d be similarly guided.
There’s no other way to implement this function (with the noted exceptions).
And if you’re thinking that’s too much of a coincidence to not scratch a larger concept, you’re right (or also guessed from the section title)!
The concept is the (Never) -> NewOutput
, (Never) -> NewFailure
, or more generally, (Never) -> T
closures for an arbitrary type, T
. No matter which T
you have in hand, the only way to map from Never
to it is our new friend { _ -> T in }
.
Put another way, there is a unique (Never) -> T
closure per T
in Swift’s type system—making Never
a “starting point” each type can be uniquely mapped from.
Mathematicians call such starting points initial objects.
In their words,
An initial object of a category C is an object I in C such that for every object X in C, there exists precisely one morphism I → X.
Replacing C with Swift1, I with Never
, X with T
, and “morphism” with “closure,” it’s fair to say Never
is an initial object.
Further, since there is a unique closure from Never
to any other type, that means Apple’s implementation of setFailureType
—somewhere underneath the Publishers.SetFailureType
hood—contains that mapError { _ -> NewFailure in }
call.
Which is wicked because even though Combine is closed-source, initiality (i.e. Never
being an initial object) forces the implementation.
⬦
setOutputType
’s single expression definition packs almost a thousand words-worth of detail behind its signature and usage.
Further, Never
is only one side of the coin. Can you think of a type in the Standard Library that has unique closures towards it? That is, which type, ???
, in Swift has a unique function (T) -> ???
, per type T
?
It’s a fun exercise to think on.
Until next time.
■
Footnotes
-
Calling Swift a category comes with asterisks. Still, it’s a trove from which we can translate decades-worth of learnings from category theory into everyday programming. ↩