5

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 ?

jhegedus
  • 20,244
  • 16
  • 99
  • 167
  • 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 Sep 26 '15 at 07:28
  • 1
    here is a good picture about callback hell http://seajones.co.uk/content/images/2014/12/callback-hell.png – jhegedus Sep 26 '15 at 07:37
  • 1
    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 Sep 26 '15 at 07:40
  • dead link: http://seajones.co.uk/content/images/2014/12/callback-hell.png – Will Ness Aug 11 '19 at 17:28

2 Answers2

4

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.

Petr
  • 62,528
  • 13
  • 153
  • 317
  • Very interesting. So basically - loosely speaking - the continuation monad is similar to RX. – jhegedus Sep 26 '15 at 17:30
  • FRP is good for modelling state machines, I wonder if Continuation Monad is good for that too ? – jhegedus Sep 26 '15 at 17:33
  • @jhegedus It's important to note that the continuation monad has nothing to do with asynchronous computations, there are no threads involved. It'd be possible to include threads explicitly in `ContT r IO`, or use Haskell's parallelism, but that's separate from `Cont`/`ContT`. Regarding FRP, I'd argue that unless the state machines involve time in some way, FRP is not the right tool for them, although I'm not an expert in that area. I'd suggest to look into something like [`Automaton`](http://stackoverflow.com/a/17250253/1333025). – Petr Sep 26 '15 at 19:17
  • We had a few meetups on FRP and that is how I know that FRP is pretty good for modeling state machines http://www.meetup.com/Helsinki-Functional-Reactive-Programming-Meetup/ , this is even mentioned in the lecture videos which are online, given by one of the Haskell FRP authors , time is always discrete in practice, on our computers, so in practice time evolution is just another way of describing a state machine. In practice if you want to solve a differential equation you discretize it in some way, or you find an analytic solution. The latter is not so easy in general :) – jhegedus Sep 27 '15 at 07:46
  • @jhegedus Nice, I didn't know about that. It's always good to learn something :). Do you perhaps have some more links on the topic of FRP & state machines (preferably in text form)? – Petr Sep 27 '15 at 07:47
  • The Automaton is pretty similar to Yampa which is again FRP. We had a discussion about that, and I remember that we came to that conclusion that Yampa is a beefed up Automaton. – jhegedus Sep 27 '15 at 07:48
  • Here you can find the discussion thread about this topic https://gitter.im/helsinki-frp/forall and even ask away if you like coz the people there can tell you all the details you want to know :) – jhegedus Sep 27 '15 at 07:50
  • If you search for the word automaton in the upper right corner then you find the May 18 entry. – jhegedus Sep 27 '15 at 07:52
  • This talk goes into details about Yampa https://www.youtube.com/watch?v=T7XwTolu9YI which is pretty much a beefed up Automaton - so I heard - from people who are much more clever then me :) – jhegedus Sep 27 '15 at 07:59
  • @jhegedus Interesting. I'd be interested exactly what's the difference between FRP and and an automaton (in what way it's "beefed up"), if there is one. – Petr Sep 27 '15 at 09:14
  • I guess if you ask this question on that gitter chat you will get a quick and accurate answer : ) – jhegedus Sep 27 '15 at 10:50
  • @PetrPudlák The continuation monad actually can have something to do with time when your `r` is a value representing an effect functor that incorporates passage of time (suppose `IO ()`). For example, you could have: `delay :: TimeSpan -> v -> (v -> IO ()) -> IO ()`, or `readFile :: FilePath -> (Buffer -> IO ()) -> IO ()`. If you look at the standard library in Node JS, you'll see that it's basically 100% continuation monad with impure function arrows and an unfortunate argument order. – Asad Saeeduddin Feb 01 '19 at 16:56
  • I have an article [here](https://dev.to/masaeedu/actually-callbacks-are-fine-l4g) about how to use the continuation monad to deal with asynchronicity in JS if you're interested. – Asad Saeeduddin Feb 01 '19 at 16:57
2

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 }))
Asad Saeeduddin
  • 46,193
  • 6
  • 90
  • 139