Possible option for chain dispatchers #1051
Open
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Here's a possible approach to #1023 (chain dispatchers) to play around with - see what you think. Should work fine, but still WIP with no docs, not to be merged yet.
This is essentially the "you have to confirm the chain dispatcher when you get near the end" approach, but there are a couple of points that I think will make it nicer than you might initially expect.
First, I think it might help to reframe the existing default system for dispatchers a bit. This will clean up some existing ambiguities and also pave the way for a simpler and more understandable version of chain dispatchers. The main points are:
Global defaults must be set before use: You've mentioned that
conf.Q
was never really intended to be a dynamic variable that you switched around on the fly. Since chain-specific dispatchers are a better way of achieving this same effect, we may as well reset the default system back to the original intent and require that you set the defaults before creating any promises. In this PR, setting the defaults after creating promises just produces a warning, but in a future version of PromiseKit it could be a hard error. The point is to clearly distinguish between global and local effects and to remove any ambiguity about when dispatching choices are actually madePhase out the terms
map
andreturn
: Currently, every function fits in one of these categories and default dispatching varies depending on the function. But the terms are a bit vague, both becausemap
andreturn
aren't very descriptive and because you pretty much just have to know which functions are which. Mostmap
functions aren't really map operations, and mostreturn
functions don't actually return anything.Dispatching becomes an attribute of the chain, not the function: Instead of having
map
andreturn
, we can reframe the system slightly to direct the focus away from functions and onto promise chains. Instead of saying that every function has an inherent dispatcher category, we can say that every promise chain has a "body" and a "tail" and that those regions have different default dispatchers. Once you enter the tail by callingcatch
,done
, orfinally
(and only one of those three), you are in the tail for good. Your default dispatching will from that point on always be the tail dispatcher (formerlyreturn
).This model tells a clearer story about how the average use case does background work in some vaguely-defined ether and then surfaces the results to the UI for action. There's only one surfacing point; the chain doesn't porpoise back and forth between dispatchers unless you specifically ask for this. This is both a conceptual model change and a small change in actual behavior.
get
andensure
are no longer special: As a corollary to this definition of a chain's tail,get
andensure
(along withensureThen
), which were formerlyreturn
functions, will now just be general functions. Like most functions, if you use them within the tail of a chain, they'll be dispatched on the tail dispatcher. If you use them in the body, they'll be dispatched as body functions.conf.Q
deprecated and replaced with a function:conf.Q
(but notconf.D
) will remain around for backward compatibility, but the standard way to set default dispatchers will be a function call as shown below. This change allows for wrappers that accommodateDispatchQueue
dot notation.OK, that's the background context. On top of this, we can add chain-specific dispatchers (which I'll just call "chain dispatchers" here, although there may be a better term.)
dispatch(on:)
: To set the chain default (from this point forward), just calldispatch(on:)
within the chain.Permanent until you change it: A dispatcher set with
dispatch(on:)
remains in effect until it's canceled withdispatch(on: .default)
, you set a different default dispatcher, or the chain terminates. (Of course, any individual function can have its ownon:
argument to override the chain default for that call only. But that does not cancel an underlying chain dispatcher.) There's only a single chain dispatcher; it doesn't vary depending on whether you're in the body or the tail.on: .default
is general: All individual functions now also accepton: .default
, which selects one of the two global default dispatchers depending on where you happen to be in the chain.You have to "help" the chain dispatcher to cross the body/tail boundary: When a chain dispatcher has been set, the first use of the chain dispatcher within the tail must be explicit. The standard way to do this is to just specify
on: .chain
as the argument to the first tail function. Alternatively, you can calldispatch(on: .chain)
, set a new chain dispatcher, or terminate the chain dispatcher withdispatch(on: .default)
. These are all fine; it doesn't matter as long as you don't use the chain dispatcher implicitly (typically by failing to specify anyon:
clause at all).If you fail to confirm a chain dispatcher, PromiseKit prints a warning. However, the chain dispatcher remains in effect and the actual behavior is unaffected. You can silence the warning by adding an explicit
on: .chain
or by settingconf.requireChainDispatcherConfirmation
tofalse
to waive this requirement globally.In the example chain below, everything from
map
onward is dispatched on the sameCoreDataDispatcher
. The chain dispatcher is properly confirmed bycatch(on: .chain)
, so no warning is printed.Pitch points:
Single point of control: Because there's only one interface between the body and the tail of any given chain, there is only one, well-defined point that potentially prints an error or requires confirmation. And that point is easy to find: it's the first instance of
done
,catch
, orfinally
.Fits well with functions that return promises: In most cases, promise chains returned by lower-level code will not have entered the tail segment, and the client will add a
done
orcatch
block to create a tail. If the lower-level code has set a chain dispatcher and the client does not confirm it, the client will be warned. At the same time, the lower-level code does have the option to set a chain dispatcher.Low confirmation burden: Adding
on: .chain
to an existing function achieves confirmation en passant without a lot of ceremony. You're guaranteed to have theon:
argument available for this purpose; if you want to use a different dispatcher, go right ahead and do a normalon:
. You're not required to confirm until (and if) you go back to the chain dispatcher. The only rule is that the first use of the chain dispatcher within the tail must be explicit.Easy to explain: The system pretty much boils down to "when you set a chain dispatcher, that's the dispatcher you get until you say otherwise." There are no subtleties regarding
map
vs.return
, or multiple dispatchers, or segments of a chain, or operations that terminate your chain dispatcher as an unexpected side effect. But at the same time, if you don't seem to be aware of the basic distinction between the body and tail of a chain, or you don't seem to realize that someone has handed you a chain with an embedded dispatcher, PromiseKit will draw your attention to this with a warning.When I converted some existing code to this scheme, I was a bit surprised to find that most simplification actually came from the body/tail idea rather than from chain dispatchers themselves.
ensure
andget
are useful anywhere in a chain, so it's nice that they now self-adapt and do the right thing depending on context.As far as implementation, it's a pretty clean change. Every thenable has a
DispatchState
struct that tracks progress along the chain and manages the chain dispatcher, if there is one. All the logic for managing dispatching goes inDispatchState
; the promise functions don't have to do anything but propagate the state to their children. Cancellable promises don't really have to do much at all since they inherit most of this behavior from their wrapped promises.There's another interesting wrinkle in this PR, but I'll leave it for later... Everything above should work as described.
Thoughts?