What is "callback hell" and how and why does RX solve it?
1) What is a "callback hell" for someone who does not know javascript and node.js ?
This other question has some examples of Javascript callback hell: How to avoid long nesting of asynchronous functions in Node.js
The problem in Javascript is that the only way to "freeze" a computation and have the "rest of it" execute latter (asynchronously) is to put "the rest of it" inside a callback.
For example, say I want to run code that looks like this:
x = getData();
y = getMoreData(x);
z = getMoreData(y);
...
What happens if now I want to make the getData functions asynchronous, meaning that I get a chance to run some other code while I am waiting for them to return their values? In Javascript, the only way would be to rewrite everything that touches an async computation using continuation passing style:
getData(function(x){
getMoreData(x, function(y){
getMoreData(y, function(z){
...
});
});
});
I don't think I need to convince anyone that this version is uglier than the previous one. :-)
2) When (in what kind of settings) does the "callback hell problem" occur?
When you have lots of callback functions in your code! It gets harder to work with them the more of them you have in your code and it gets particularly bad when you need to do loops, try-catch blocks and things like that.
For example, as far as I know, in JavaScript the only way to execute a series of asynchronous functions where one is run after the previous returns is using a recursive function. You can't use a for loop.
// we would like to write the following
for(var i=0; i<10; i++){
doSomething(i);
}
blah();
Instead, we might need to end up writing:
function loop(i, onDone){
if(i >= 10){
onDone()
}else{
doSomething(i, function(){
loop(i+1, onDone);
});
}
}
loop(0, function(){
blah();
});
//ugh!
The number of questions we get here on StackOverflow asking how to do this kind of thing is a testament to how confusing it is :)
3) Why does it occur ?
It occurs because in JavaScript the only way to delay a computation so that it runs after the asynchronous call returns is to put the delayed code inside a callback function. You cannot delay code that was written in traditional synchronous style so you end up with nested callbacks everywhere.
4) Or can "callback hell" occur also in a single threaded application?
Asynchronous programming has to do with concurrency while a single-thread has to do with parallelism. The two concepts are actually not the same thing.
You can still have concurrent code in a single threaded context. In fact, JavaScript, the queen of callback hell, is single threaded.
What is the difference between concurrency and parallelism?
5) could you please also show how RX solves the "callback hell problem" on that simple example.
I don't know anything about RX in particular, but usually this problem gets solved by adding native support for asynchronous computation in the programming language. The implementations can vary and include: async, generators, coroutines, and callcc.
In Python we can implement that previous loop example with something along the lines of:
def myLoop():
for i in range(10):
doSomething(i)
yield
myGen = myLoop()
This is not the full code but the idea is that the "yield" pauses our for loop until someone calls myGen.next(). The important thing is that we could still write the code using a for loop, without needing to turn out logic "inside out" like we had to do in that recursive loop
function.
To address the question of how Rx solves callback hell:
First let's describe callback hell again.
Imagine a case were we must do http to get three resources - person, planet and galaxy. Our objective is to find the galaxy the person lives in. First we must get the person, then the planet, then the galaxy. That's three callbacks for three asynchronous operations.
getPerson(person => {
getPlanet(person, (planet) => {
getGalaxy(planet, (galaxy) => {
console.log(galaxy);
});
});
});
Each callback is nested. Each inner callback is dependent on its parent. This leads to the "pyramid of doom" style of callback hell. The code looks like a > sign.
To solve this in RxJs you could do something like so:
getPerson()
.map(person => getPlanet(person))
.map(planet => getGalaxy(planet))
.mergeAll()
.subscribe(galaxy => console.log(galaxy));
With the mergeMap
AKA flatMap
operator you could make it more succinct:
getPerson()
.mergeMap(person => getPlanet(person))
.mergeMap(planet => getGalaxy(planet))
.subscribe(galaxy => console.log(galaxy));
As you can see, the code is flattened and contains a single chain of method calls. We have no "pyramid of doom".
Hence, callback hell is avoided.
In case you were wondering, promises are another way to avoid callback hell, but promises are eager, not lazy like observables and (generally speaking) you cannot cancel them as easily.
Just answer the question: could you please also show how RX solves the "callback hell problem" on that simple example?
The magic is flatMap
. We can write the following code in Rx for @hugomg's example:
def getData() = Observable[X]
getData().flatMap(x -> Observable[Y])
.flatMap(y -> Observable[Z])
.map(z -> ...)...
It's like you are writing some synchronous FP codes, but actually you can make them asynchronous by Scheduler
.