5
votes

Here and here it is said that the Continuation Monad solves the callback hell.

RX and FRP also solve the Callback hell.

If all these three tools solve the Callback hell then the following question arises:

In Erik's video it is said that RX=Continuation Monad. Is that really true? If yes, could you show the mapping?

IF RX is not = Cont. Monad then what is the difference between RX and Continuation Monad?

Similarly, what is the difference between FRP and the Continuation Monad ?

In other words, assuming that the reader knows what FRP or RX is, how can the reader easily understand what the Continuation Monad is ?

Is it possible/easy to understand what the Continuation Monad is by comparing it with RX or FRP ?

2
first what is the callback-hell? Second: are you talking about FRP or about frameworks like RX (those are not really implementations of FRP) - also some of the parts (for example the type) of the cont-monad look like an event (you feed event-handler) but for example there is no unsubscribe (you usually find on frameworks like RX) and usually you don't add more than one continuations ;) ... so IMO it's not "the same" ... but well it's my opinion and maybe this is not the right place for discussions like this(?)Random Dev
here is a good picture about callback hell seajones.co.uk/content/images/2014/12/callback-hell.pngjhegedus
in Erik's video it is said that RX is a realization of the Cont. Monad, it would be good to know "in simple terms"/"explained to mere mortals" why and how that is true .jhegedus

2 Answers

4
votes

I'm not familiar with RX, but regarding FRP and the continuation monad, they're fundamentally different concepts.

Functional reactive programming is a solution to the problem of working with time-dependent values and events. With events, you can solve the callback problem by sequencing computations in such a way that when one finishes, an event is sent and it triggers the next one. But with callbacks you really don't care about time, you just want to sequence computations in a specific way, so unless your whole program is based on FRP, it's not the optimal solution.

The continuation monad has nothing to do with time at all. It's an abstract concept of computations that can (loosely speaking) take "snapshots" of the current evaluation sequence and use them to "jump" to those snapshots arbitrarily. This allows to create complex control structures such as loops and coroutines. Now have a look at the definition of the continuation monad:

newtype Cont r a = Cont { runCont :: (a -> r) -> r}

This is essentially a function with a callback! It's a function that accepts a continuation (callback) that will continue the computation after a is produced. So if you have several functions that take continuations,

f1 :: (Int -> r) -> r

f2 :: Int -> (Char -> c) -> c

f3 :: Char -> (String -> d) -> d

their composition becomes somewhat messy:

comp :: String
comp = f1 (\a -> f2 a (\b -> f3 b id))

Using continuations, it becomes very straightforward, we just need to wrap them in cont and then sequence them using the monadic bind operaiton >>=:

import Control.Monad.Cont

comp' :: String
comp' = runCont (cont f1 >>= cont . f2 >>= cont . f3) id

So continuations are a direct solution to the callback problem.

0
votes

If you look at how the continuation monad is defined in Haskell, it looks like this:

data Cont r a =  Cont { runCont :: (a -> r) -> r }

By itself, this is completely pure, and doesn't represent real world effects or time. Even in its current form it can be used to represent temporal/IO effects, simply by choosing r to be a type involving IO. For our purposes however, we're going to do something slightly different. We're going to substitute the concrete -> with a type parameter:

data Cont p r a = Cont { runCont :: p (p a r) r }

What is the use of this? In Haskell, we only have pure functions which map some input domain to an output domain. In other languages, we can have such functions, but we can additionally define impure "functions", which (in addition to producing some arbitrary output for a given input), may implicitly perform side effects.

Here is an example of both in JS:

// :: Int -> Int -> Int
const add = x => y => x + y

// :: String -!-> ()
const log = msg => { console.log(msg); }

Note that log is not a pure function that produces a value representing an effect, which is how such things are encoded in pure languages like Haskell. Instead, the effect is associated with the mere invocation of log. To capture this, we can use different arrows when we talk about pure functions and impure "functions" (-> and -!->, respectively).

So, returning to your question about how the continuation monad solves callback hell, it turns out that (in JavaScript at least), most of the APIs that give rise to callback hell can be massaged quite easily into values of the form Cont (-!->) () a, which I'll henceforth refer to as Cont! a.

Once you have a monadic API, the rest is easy; you can traverse structures full of continuations into continuations of a structure, use do notation to write multi-step computations akin to async/await, use monad transformers to equip continuations with additional behavior (for example error handling), etc.

The monad instance looks much the same as it does in Haskell:

// :: type Cont p r a = p (p a r) r
// :: type Cont! = Cont (-!->) ()

// :: type Monad m = { pure: x -> m x, bind: (a -> m b) -> m a -> m b }

// :: Monad Cont!
const Cont = (() => {
  // :: x -> Cont! x
  const pure = x => cb => cb(x)
  // :: (a -> Cont! b) -> Cont! a -> Cont! b
  const bind = amb => ma => cb => ma(a => amb(a)(cb))

  return { pure, bind }
})()

Here are a couple of examples of modelling the setTimeout and readFile APIs available in Node JS as impure continuations:

// :: FilePath -> Cont! (Either ReadFileError Buffer)
const readFile = path => cb => fs.readFile(path, (e, b) => cb(e ? Left(e) : Right(b)))

// :: Int -> v -> Cont! v
const setTimeout = delay => v => cb => setTimeout(() => cb(v), delay)

As a contrived example, here's the "callback hell" we enter when we use the standard APIs to read a file, wait five seconds, then read another file:

fs.readFile("foo.txt", (e, b1) => {
  if (e) { throw e }
  setTimeout(() => {
    fs.readFile("bar.txt", (e, b2) => {
      if (e) { throw e }
      console.log(b1.toString("utf8") + b2.toString("utf8"))
    })
  }, 5000)
})

And here is the equivalent program using the continuation monad:

const ECont = EitherT(Cont)

const decode = buf => buf.toString("utf8")

const foo = readFile("foo.txt") |> ECont.map(decode)
const bar = readFile("bar.txt") |> ECont.map(decode)

// Imaginary do notation for JS for purposes of clarity
const result = do(ECont)([
  [s1, foo],
  ECont.lift(delay_(5000)),
  [s2, bar],
  ECont.pure(s1 + s2)
])

const panic = e => { throw e }
const log = v => { console.log(v) }

// No side effects actually happen until the next line is invoked
result(Either.match({ Left: panic, Right: log }))