Beautiful Code [95]
But where to implement the flag checks? We could hardcode them into the delta editor, the one returned (by reference) from Get_Update_Editor(). But that's obviously a poor choice: the delta editor is a library function that might be called from code that wants a totally different style of cancellation checking, or none at all.
A slightly better solution would be to pass a cancellation-checking callback function and associated baton to Get_Update_Editor(). The returned editor would periodically invoke the callback on the baton and, depending on the return value, either continue as normal or return early (if the callback is null, it is never invoked). But that arrangement isn't ideal either. Checking cancellation is really a parasitic goal: you might want to do it when updating, or you might not, but in any case it has no effect on the way the update process itself works. Ideally, the two shouldn't be tangled together in the code, especially as we had concluded that, for the most part, operations didn't need fine-grained control over cancellation checking, anyway—the editor call boundaries would do just fine.
Cancellation is just one example of an auxiliary task associated with tree delta edits. We faced, or thought we faced, similar problems in keeping track of committed targets while transmitting changes from the client to the server, in reporting update or commit progress to the user, and in various other situations. Naturally, we looked for a way to abstract out these adjunct behaviors, so the core code wouldn't be cluttered with them. In fact, we looked so hard that we initially over-abstracted:
/** Compose editor_1 and its baton with editor_2 and its baton.
*
* Return a new editor in new_editor (allocated in pool), in which
* each function fun calls editor_1->fun and then editor_2->fun,
* with the corresponding batons.
*
* If editor_1->fun returns error, that error is returned from
* new_editor->fun and editor_2->fun is never called; otherwise
* new_editor->fun's return value is the same as editor_2->fun's.
*
* If an editor function is null, it is simply never called, and this
* is not an error.
*/
void
svn_delta_compose_editors(const svn_delta_editor_t **new_editor,
void **new_edit_baton,
const svn_delta_editor_t *editor_1,
void *edit_baton_1,
const svn_delta_editor_t *editor_2,
void *edit_baton_2,
apr_pool_t *pool);
Although this turned out to go a bit too far—we'll look at why in a moment—I still find it a testament to the beauty of the editor interface. The composed editors behaved predictably, they kept the code clean (because no individual editor function had to worry about the details of some parallel editor invoked before or after it), and they passed the associativity test: you could take an editor that was itself the result of a composition and compose it with other editors, and everything would just work. It worked because the editors all agreed on the basic shape of the operation they were performing, even though they might do totally different things with the data.
As you can tell, I still miss editor composition for its sheer elegance. But