I was writing some Core Bluetooth code on iOS when the dragon appeared.
A callback looks like a function. But it’s not a function; it’s an event disguised as a function.
Once you know the beast’s weakness, it’s simple, though not easy.
Don’t write callbacks. Write code to handle events. All events implicated by a particular objective. That code might be a state machine, or it might be something more complex. It’s much easier to debug a state machine than a callback dragon.
The callback dragon is as fierce as the goto serpent.
Dropping the mythical beast metaphor
Framework-based code overuses delegation (OO speak for callbacks), so that if you’re not expecting it, your callbacks will be doing way too much. They get called irrespective of context and you have to reconstruct the context. A colleague of mine did it on Android (in Java) with flags. It was a big mess (no offense to my colleague; he was in a hurry), but I discerned that it was a state machine, if a buggy and unreadable one. I felt pressured to go the same way, but my love of simplicity and correctness got the best of me.
I wrote down the roughly dozen callbacks in use, created event enums for each one and data structs for some of them that needed it, ripped the guts out of the callbacks and transplanted them into state functions that take a self pointer and an event struct.
State functions have several useful properties:
- they can receive an event generated by any callback
- they explicitly return the next state as a function pointer
- they succinctly encode behavior and context in one state variable
The first property requires some overhead, checking the event type first, but the power gained—the ability to process a union of events—is worth it. It immediately revealed and allowed me to fix bugs and limitations in my design. Printing an error message when you get an unexpected event is much more useful than silently messing up.
The state machine wasn’t totally right at first, and I got some pointer crashes. But the errors were debuggable by going up and down the stack to see that there was some discrepancy. There may be a way to improve type safety later.
Sure, I had to add 200 lines of code, but it’s ultimately simpler, because the callbacks have been decoupled from the event handling. The result is code that’s a lot more like the state machines I’ve written in Go (as goroutines) and C (for a lexer). Hell, it basically is written in C. Objective-C classes just aren’t that useful to me.
It should surprise no one that the proof of concept was written in Go in about five minutes.
I wasted probably half an hour trying to figure out how to do this refactoring in the Objective-C idiom, but I failed. Selectors? NSInvocation? Class? Not that I care too much. Objective-C and Cocoa are a nonorthogonal junk pile. The best part of using them is being able to write C.