Nonsense Accusations of Spaghetti Code Considered Harmful

I have no beef with callbacks. However primitive they may be, I appreciate their simplicity, and I consider them a technique worth learning and using.

A number of people I respect have recently voiced their support for the article Escape from Callback Hell. As it happens, not only do I like many of the people who have endorsed it, I also like the Elm programming language (whose author presumably posted it).

However, I also like honesty. And in the spirit of honesty, I feel obliged to point out that this article is high-octane nonsense.

If you have worked with AJAX or node.js or any other callback heavy framework, you have probably been to Callback Hell. Your whole application ends up being passed around as callbacks, making the code extremely difficult to read and maintain. The resulting tangled mess of code is often pejoritively called spaghetti code, a term borrowed from the days of goto.

In case you don’t know what this is referring to, here’s an example of some synchronous (“normal”) code in CoffeeScript:

someFunction "argument", 1, 2
anotherFunction "another argument", 5, 6

In contrast, here is the dreaded callback version:

someFunction "argument", 1, 2, ->
  anotherFunction "another argument", 5, 6

As you can see, the callback makes this code “extremely difficult to read and maintain.” Whereas the first example is perfectly readable, the second example is clearly a “tangled mess of code” unfit for human consumption.

Er.

Okay, so unless you have a pathological aversion to indentation, the objection clearly doesn’t apply with this trivial two-line example…but let’s read on to see if the article raises the more common callback complaint: the infamous “callback pyramid”, where nested callbacks lead to so much indenting that you can make out an entire implied triangle protruding from the left margin.

Just like goto, these callbacks force you to jump around your codebase in a way that is really hard to understand. You basically have to read the whole program to understand what any individual function does.

No mention of the pyramid yet, but hang on—you have to do what with the what now?

Let’s assume that it’s good practice to organize your program into simple functions that have as few side effects as possible. If that’s the case, then if you “have to read the whole program to understand what an individual function does,” the diagnosis is that your functions are poorly written.

There is no law against writing simple, concise callbacks that defer to well-organized, side-effect-free functions for complex processing. In fact, I’d recommend using them in precisely that way. And once you’ve done that, the above paragraph no longer describes your callbacks.

This claim is plainly false. Callbacks in no way “force you” to engage in any of that badness.

And good luck if you want to add something to your code. A change in one function may break functions that appear to be unrelated (the functions may never even appear together in the entire codebase, connected only by deeply nested callbacks). You’ll usually find yourself carefully tracing through the entire sequence of callbacks to find out what your change will really do.

If this bit describes something smelly to you, what you are smelling is side effects.

It is indeed a downer when “a change in one function may break functions that appear to be unrelated.” That’s why it’s good practice to minimize side effects in your functions.

But callbacks are only one of many ways to sequence side effects. Why apply such a broad concern only to callbacks in particular? This is like saying “poorly organized Haskell code sucks, therefore Haskell sucks.” No, poorly organized code sucks. If that code happens to be written in Haskell, don’t shoot the messenger.

If you are not convinced that callbacks and goto are equally harmful, read Edsger Dijkstra’s famous “Go To Statement Considered Harmful” and replace the mentions of goto with mentions of callbacks.

Doing this makes poor Dijkstra out to be a lunatic raving about the importance of eliminating callbacks (which employ procedure calls) and instead employing…procedure calls.

And let’s not overlook the dishonesty of suggesting “take this article decrying a bad thing, substitute what I’m opposing for the bad thing, and watch how bad that makes it look!”

Yay, we solved the problem of the site freezing, but now our code is a mess of deeply nested functions that is annoying to write and unpleasant to read and maintain. This is a glimpse into Callback Hell.

Aha! Our first glimpse into Callback Hell.

Let’s have a look at The Code of the Beast:

function getPhoto(tag, handlerCallback) {
  asyncGet(requestTag(tag), function(photoList) {
    asyncGet(requestOneFrom(photoList), function(photoSizes) {
      handlerCallback(sizesToPhoto(photoSizes));
    });
  });
}

getPhoto('tokyo', drawOnScreen);

Since this is an article about callbacks, not JavaScript—JS is only used for examples, except where it’s briefly mentioned in the Conclusion—allow me to rewrite this in CoffeeScript, so that the comparison to Elm (which, like CoffeeScript, is much syntactically cleaner than JavaScript) is at least fair.

getPhoto = (tag, handlerCallback) ->
  asyncGet requestTag(tag), (photoList) ->
    asyncGet requestOneFrom(photoList), (photoSizes) ->
      handlerCallback sizesToPhoto(photoSizes)

getPhoto "tokyo", drawOnScreen

As the article notes, “now our code is a mess of deeply nested functions that is annoying to write and unpleasant to read and maintain. This is a glimpse into Callback Hell.”

Indeed: to gaze upon this four-line function with three indentations is to peer tremblingly into the gaping maw of the Abyss itself. I recommend a seatbelt when dealing with code this Hellacious.

The article then goes on to describe Functional Reactive Programming (which actually is nice stuff, the surrounding fire and brimstone about callbacks notwithstanding), and cites the following example as a much cleaner alternative to Callback Hell:

getPhotos tags =
  let photoList = send (lift requestTag tags) in
  let photoSizes = send (lift requestOneFrom photoList) in
    lift sizesToPhoto photoSizes

As anyone can plainly see, whereas the previous four-line function with three indentations was about enough to curdle Cerberus’s nose hairs, the above four-line function with only two indentations is really quite a thing of beauty.

Are these claims for real?

Look, plenty of people can’t stand callbacks—just like plenty of people can’t stand monads. If that’s your personal preference, then cool, use something else. But “callbacks are the modern goto”?

Come on.

Scattering interdependent functions across a code base should be avoided for its own sake, and nothing about callbacks requires that you use them that way. Trying to conflate the two in order to scare people away from learning a perfectly usable technique does more harm than good to our discipline. 

Are there more enticing ways to sequence asynchronous effects than callbacks? I think so, yeah. And that’s the only good reason I’ve heard to avoid them: a preference for something else.

Regardless, they’re definitely worth learning, and there are plenty of cases where they’re the right tool for the job. Don’t let “Escape from Callback Hell” dissuade you.

-@rtfeldman

PS: Separately, Elm actually is a very cool project. Check it out.

Notes

  1. evandrix reblogged this from rtfeldman
  2. rtfeldman posted this