Callbacks are a problem with syntax not semantics

We’ve all heard about callback hell, of javascript fame. Using callbacks allows us to chain together actions that might fail using continuation style, that is to say, in addition to arguments, functions are also given functions to run when they succeed or fail. People do not like this b/c every callback increases total indentation – and programmers hate indentation.

function example() {
  fetchA((a) => {
    fetchB(a, (b) => {
      fetchC(b, (c) => {
        fetchD(c, (d) => {
          // do something with d
        })
      })
    })
  })
}

Consider an alternative to continuation passing style: async/await.

async function example() {
  const a = await fetchA()
  const b = await fetchB(a)
  const c = await fetchC(b)
  const d = await fetchD(c)
  // do something with d
}

This is fine but it runs afoul of the (function coloring problem)[https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/]. The fetch functions are now not normal functions but async ones and they do not compose with non-async functions. This tension has resulted in many JS libraries providing both synchronous and promise-based apis. Boo.

Ok, so we want neither coloring nor too much indentation. Callbacks don’t change function color and they only add indentation b/c of JS syntax rules. Let’s look at some languages that handle callbacks without adding indentation.

Haskell’s do notation:

example :: IO ()
example = do
  a <- fetchA
  b <- fetchB a
  c <- fetchC b
  d <- fetchD c
  -- do something with d

Gleam’s use syntax

pub fn example() -> Result(String, Nil) {
  use a <- fetch_a
  use b <- fetch_b(b, _)
  use c <- fetch_c(c, _)
  use d <- fetch_d(d, _)
  // do something with d
}

Both do and use notation desugar to vanilla nested callbacks.