Live data from Hacker News

Callback Hell (2016)

callbackhell.com

141–150 of 170 posts

Re: Callback Hell (2016)

#141
post #49

The 'asynchonous problem' in JS is twofold. It makes it more difficult to reason about the program, and is hell on readability, and readbility matters. Support, maintenance, etc. The lack of an ability to structure async ops in a simple way is the biggest drawback in the language. Callbacks of course have the drawbacks mentioned. Sure, you can separate the functions, smaller functions and that too is preferred, but s…

Promises are just as bad as callbacks in most of the code organization issues and I agree with you that they don't actually solve "callback hell".

However, one thing that promises (and other library-based solutions) do better than hand-written callbacks is exception handling. By default, promises will capture and propagate and exceptions thrown inside them, which is something that is very easy to forget to do with hand-written callbacks.

Re: Callback Hell (2016)

#142
post #58

Cont, the mother of all monads [0] :) [0] http://blog.sigfpe.com/2008/12/mother-of-all-monads.html?m=1

This is the one of the many things that are anti-features in most languages, but become powerful best-practice tools when applied to Haskell.

Re: Callback Hell (2016)

#143

As a curious and perhaps naive aside, I've been wondering why the programmer should need to care about asynchronous execution of code at all. Can't it all be abstracted under a procedural layer and let the OS worry about not blocking anything? The advent of promises, async.js, and other paradigms tell me that people still kind of want to write code that does one thing after another, then another, then another.

> Can't it all be abstracted under a procedural layer and let the OS worry about not blocking anything?

That's exactly what coroutines do.

Here is an in-depth description of how Kotlin implements them:

https://github.com/Kotlin/kotlin-coroutines/blob/master/kotl...

Re: Callback Hell (2016)

#144
post #75

As a curious and perhaps naive aside, I've been wondering why the programmer should need to care about asynchronous execution of code at all. Can't it all be abstracted under a procedural layer and let the OS worry about not blocking anything? The advent of promises, async.js, and other paradigms tell me that people still kind of want to write code that does one thing after another, then another, then another.

> Can't it all be abstracted under a procedural layer and let the OS worry about not blocking anything? No. There's no way around understanding that some of your code will run now, and some will run later. It's imperative to understand this in places where you mix sync and async code. It's not possible to avoid mixing them, after all, the async functions have to be called by something . You could hide all async in a…

> > Can't it all be abstracted under a procedural layer and let the OS worry about not blocking anything?

> No.

Yes. That's exactly what coroutines achieve.

They transparently suspend and resume execution of async code in order to make it look imperative.

See https://github.com/Kotlin/kotlin-coroutines/blob/master/kotl... for a good example.

Re: Callback Hell (2016)

#145
post #79

I think this is a fantastic article in many ways. It clearly describes the issues of callback hell and gives simple examples on how to clean it up. Modern JavaScript has gotten very complicated lately because it's so flexible. Many people are developing interesting frameworks to solve niche problems; however, it feels like many of these solutions are overly complicated outside the niche. Yet, developers are adopting…

Maybe it's because I'm a C programmer and not familiar with js but I felt this article did a very poor job of describing "callback hell." I still don't have a clear idea of what it is. The author shows some code with lots of nested if/else clauses and claims this is bad because it blocks; fair enough. Then they explain what callbacks are and that some people have trouble understanding the asynchronous nature. Then th…

Hmm. Lemme see if I can explain this.

Javascript uses "function references" in a similar fashion to C's "function pointers". This...

  function fooAllTheBars() { /* ...quux... */ }
...is a function declaration statement which binds a callable reference to my function to the name `fooAllTheBars`. That said, we can express an "anonymous function declaration" as an rvalue, so we can also bind the function to a name ourselves:

  const fooAllTheBars = function() { /* ...quux... */ };
Since this is a function reference, I could pass it into a function like this:

  setTimeout(fooAlltheBars, 1000);
...which would tell the JS runtime to wait 1000ms and then execute `fooAllTheBars`. I could also express this using an anonymous function:

  setTimeout(function() { /* ...quux... */ }, 1000);
This is all fine and dandy, but suppose I also had a function like this:

  function quuxify(value, calculator, cb) {
    calculator(value, function(err, calcResult) {
      if (!err) {
        cb(calcResult.toFixed(2));
      }
    });
  }

  function bazzify(success, failure) {
    quuxify('foo', 
      function(value, cb) {
        setTimeout(function() {
          if (value > 100) {
            cb("too high!", null);
          } else {
            cb(null, value - 10);
          }
        }, 1000);
      }, success);
  }

  bazzify(console.log, () => {});
This is, admittedly, a bit of a pathological example than the simple "nested callback problem", and the code makes even less sense because "bazzify" and "quuxify" don't actually mean anything, but I think it's a better illustration of the problem at hand.

Basically, if you get into callback hell, your control flow starts bouncing around like a pinball hitting bumpers, and figuring out why is nearly impossible (unless you want to dig through fifteen levels of anonymous functions). And the sad part is that in most cases, you can't avoid it altogether because there's something fundamentally asynchronous like a `setTimeout` or an `xmlHttpRequest` waiting at the end of the call stack.

Re: Callback Hell (2016)

#146

Callback hell has confused me for a long time. I kept assuming there was a good reason people write code that way, and there was something wrong with naming my functions and passing them by name. The latter approach always made more sense to me - it's more readable, and more intuitive. If there is no real benefit to the nested anonymous function style, why do people do it that way? It would never even have occurred t…

I can think of one advantage - lexical closure: when your callback's body is defined inside the calling scope, you can use variables from that scope inside the callback; and the callback can assign to those variables & have the change persist in the caller (only really relevant when the callback's invoked synchronously, but can be very useful in those cases - eg, with Array.prototype.forEach).

It is possible to replicate these effects without closures, but it can get pretty messy: the former by prepending the variables to the callback's parameter list and .bind()ing them where you use it; the latter similarly, but you also have to introduce a layer of indirection via an object to get a "pass-by-reference"-like effect.

So sometimes it really is simpler to just use a function expression in-place.

To contrive a quick motivating example:

    var odds = 0, evens = 0;
    array.forEach(function (x) {
        if (x % 2) ++odds;
        else ++evens;
    });
    console.log("odd:", odds, "even:", evens);
versus

    function callback (counts, x) {
        if (x % 2) ++counts.odds;
        else ++counts.evens;
    }
    ...
    var counts = { odds: 0, evens: 0 };
    array.forEach(callback.bind(this, counts));
    console.log("odd:", counts.odds, "even:", counts.evens);
(And yes, I know you can just use "for...of" for this particular scenario in modern JS; like I said, contrived example.)

Re: Callback Hell (2016)

#147

Earlier quoted context omitted.

That's how it's done in Go. You write plain, "blocking" code, and if it blocks, the Go runtime will just schedule another goroutine (green thread). In Go there's no need for callbacks or littering your code with `await` keywords to write concurrent software.

I never checked that out but ; https://www.golang-book.com/books/intro/10 looks messy to me. Less messy than callbacks but more messy than async/await imho. Maybe there are nicer examples? Also I agree with the coroutine LUA rationale you can read in the link in this same thread. It appears that with Go I still need to alter my own code to use libraries that are written to run async or am I wrong?

>Less messy than callbacks but more messy than async/await imho. Maybe there are nicer examples?

That book chapter doesn't really illustrate the utility of Go's concurrency very well, it just explains the basic components that make it work. Let's say you wanted to fetch three text documents via HTTP and print each of them. Here's the regular, blocking way to do that (error handling, package imports, etc. omitted for brevity):

    func main() {
        documentURLs := []string{
            "http://example.org/foo.txt",
            "http://example.net/bar.txt",
            "http://example.com/quux.txt",
        }
        for _, url := range documentURLs {
            response, err := http.Get(url)
            body, err := ioutil.ReadAll(response.Body)
            fmt.Print(string(body))
        }
    }
For each URL in the documentURLs list, make a GET request to that URL, read the body of the HTTP response, and convert it to a string (from an array of bytes) and print it. First it'll fetch the first document and print, then the second, then the third. Of course, we'd prefer not to wait for the previous request to finish before we perform the next, so let's make it concurrent.

    func main() {
        documentURLs := []string{
            "http://example.org/foo.txt",
            "http://example.net/bar.txt",
            "http://example.com/quux.txt",
        }

        // `documents` is a channel of values of type `string`.
        // A channel is a safe FIFO queue.
        documents := make(chan string)

        for _, url := range documentURLs {
            go func() {
                response, err := http.Get(url)
                body, err := ioutil.ReadAll(response.Body)
                documents 
OK, let's see what's new here. First, we're making a "channel" which we will put our text documents into as we receive them. Second, the loop over documentURLs is a little different. We put the code into an anonymous function and run it with the `go` keyword. This starts the function in a "goroutine", which is like a light-weight thread. Because we run the function in a new goroutine, the loop does not wait for the anonymous function to complete and the loop continues immediately to the next URL, for which a function is again launched in a new goroutine, and so on for all the URLs. Anonymous functions are closures in Go, so we don't need to explicitly pass in the `documents` and `url` variables (actually this program is buggy, but that's a minor detail).

In the closure we make an HTTP request and read the response body, just like before, but instead of printing the result directly, we send it to the `documents` channel. Basically, we start jobs on three new threads and tell them to put the result of the work in a queue. When we have started the jobs with the first loop, we proceeed to the next loop where we read the strings being sent to the `documents` channel and print them as we receive them. Reading on a channel blocks until there is something to receive.

So I hope you'll agree that this is a pretty simple way to run things concurrently: just use multi-threading. Except we're not using OS threads, which are expensive, we are using goroutines—light-weight green threads managed by the Go runtime. You can have thousands or hundreds of thousands of goroutines running at once, OS threads don't scale that well. If you block a goroutine (e.g., when performing an HTTP request), the Go runtime will just schedule another goroutine, which is cheap. The Go runtime may use a single thread (in which case the program is concurrent but not parallel) or it may use multiple threads (in which the program is both concurrent and parallel).

If instead you were writing a network server with Go, here's how you'd do it (example from https://golang.org/pkg/net/):

    ln, err := net.Listen("tcp", ":8080")
    for {
        conn, err := ln.Accept()
        go handleConnection(conn)
    }
You listen for new TCP connections, and when you get one, you hand over the connection to a function started in a new goroutine, and then you go back to listening for new connections again. It's the simple model of one thread per connection, except with goroutines it's actually scalable. You can block as much as you want in the handler goroutine and it won't block other goroutines. You can even start additional goroutines inside your handler goroutines. For example, if you wanted to fetch the text documents from the concurrent GET example and send them to your clients, you could adapt the `main` function from the concurrent GET example just a little bit and use that as your `handleConnection` function.

>It appears that with Go I still need to alter my own code to use libraries that are written to run async or am I wrong?

You can make asynchronous library APIs with goroutines and channels: when a function is called, start the work in a goroutine and return a channel. The goroutine sends the return value on the channel. It's kind of ugly and generally frowned upon; it's cumbersome for users who want to use it synchronously, and it's no better than doing it yourself if you do want to use it asynchronously. Instead it is preferred to expose synchronous APIs, which can then be made to run concurrently as desired.

Re: Callback Hell (2016)

#148

Callback hell has confused me for a long time. I kept assuming there was a good reason people write code that way, and there was something wrong with naming my functions and passing them by name. The latter approach always made more sense to me - it's more readable, and more intuitive. If there is no real benefit to the nested anonymous function style, why do people do it that way? It would never even have occurred t…

A lot of one-time-use functions aren't necessarily more readable either. You can start to the lose the context of when, where, and why it's being called. Many small functions or nested anonymous functions are just two ways of dealing with a difficult situation.

Re: Callback Hell (2016)

#149

Earlier quoted context omitted.

> In other languages like C, Ruby or Python there is the expectation that whatever happens on line 1 will finish before the code on line 2 starts running actually, if you are into asynchronous programming (I am), I would claim the opposite. When writing an async program in C# you need to be acutely aware that in-between any two sequentially executed lines, entire threads of execution could have been invoked and compl…

You can still have race conditions anywhere IO is concerned, just not at a thread level (because no threads).

This is correct. Race conditions are still the most common vulnerability in web applications.

Take a classic example: a one-time use coupon. Without some kind of locking, two simultaneous requests would be able to use the same coupon successfully. Roughly:

  0.0s - [1] Client 1: Apply coupon
  0.0s - [2] Client 2: Apply coupon
  0.1s - [1] Server to database: Is coupon marked as used?
  0.1s - [2] Server to database: Is coupon marked as used?
  0.2s - [1] Database to server: Nope! All good.
  0.2s - [2] Database to server: Nope! All good.*
* Since the question was asked for both requests at the same time, the answer was "Not used" in both cases.

  0.3s - [1] Server to database: Update coupon as used.
  0.3s - [2] Server to database: Update coupon as used.*

         * Coupon has been used twice :(
Node's async model is not going to help you here. You will need something more, such as a mutex.

Re: Callback Hell (2016)

#150
post #78

I was looking through the vue.js documentation last night and noticed a similar code style as the one this uses. Are we back to "semi-colons are uncool"? It's so much eaier for me to glean the intent when browsing JS written with semi-colons.

This looks kinda like http://standardjs.com/, but not quite.
Post reply on HN