"Await" is fantastic, and having using it for JavaScript (via TameJS and then IcedCoffeeScript), it makes things a lot easier and clearer.
That being said, I don't think the comparison between callbacks and goto is valid.
"Goto" allows you to create horrible spaghetti-code programs, and getting rid of it forces you to structure your programs better.
"Await", fundamentally, isn't really anything more than syntactic sugar (except for exception handling, which is a good thing). "Await" doesn't change how your program is structured at all, it just changes the visual representation of your code -- from indentations in a non-linear order, to vertical and linear order. It's definitely a nice improvement, and makes code easier to understand (and allows for better exception handling), but it's not actually changing the way your program is fundamentally structured.
And finally, "await" is only applicable when a single callback gets called once at the end. If you're passing a callback that gets used repeatedly (a sorting function, for example), then normal-style callbacks are still necessary, and not harmful at all. Sometimes they can be short lambdas, sometimes they're necessarily much larger.
In sum: "await" is great, but there's nothing inherently harmful about callbacks, the way "goto" is. To the contrary -- callbacks are amazingly useful, and amazingly powerful in languages like JavaScript. "Await" just makes them nicer.
What this suggests is that "await" isn't like all structured programming, it's like one control construct. The history of structured programming is the development of more constructs as people realised that they had a goto pattern that kept popping up in their code, so they named it and thereby got a cognitively-higher-level program. Long after Dijkstra's essay, we could still occasionally find new places where goto was really the best way to do it: for instance, if you wanted to "break" out of multiple loops at once. So someone invented named break (likewise continue), and removed yet another use of the unconstrained goto in favour of a more constrained, structured construct.
Taking the OP's premise at face value, then, if callbacks are like goto, then await takes away one place where they were needed and replaces them with something more structured and safer---and this doesn't at all negate the possibility that there are other constructs yet to be invented that would continue that process.
Await does change how your program is structured, unless we're talking about most trivial cases.
You can use `await` inside a `for` loop—can you do the same with callbacks without significantly re-structuring your code?
What is the “callback analog” of placing something in a `finally` block that executes no matter which callback in a nested chain fails? You'd have to repeat that code.
Await has a potential of simplifying the structure a lot, because it befriends asynchronous operations with control flow.
>And finally, "await" is only applicable when a single callback gets called once at the end. If you're passing a callback that gets used repeatedly (a sorting function, for example), then normal-style callbacks are still necessary, and not harmful at all.
Indeed, await is only for the cases when we abuse functions (because we don't know what we'll call, and we have to bend our minds). Passing the sorting comparator is the primary use of first-class functions.
I was initially very skeptical at first too. Then I noticed that the positioning of the "Busy = false" statements had been reduced to a structured form exactly as if we had started with an unstructured GOTO or multiple exit point control flow.
As a C++ guy, I don't like his "Busy = false" system to begin with. It would seem much better (to me) if he used a non-copyable object to represent the outstanding activity. Such an object could naturally reset the "Busy" flag in its destructor. But usually there's a better scheme than using a simple Boolean flag to represent a "busy" state anyway. (How is clearing the flag going to release the next guy waiting for the resource?)
So while I'm still a bit skeptical of drawing conclusions from this, I readily admit it's not so off-the-wall as I'd originally thought.
Await is basically a continuation-passing style transform that puts the continuation in the continue handler of the task. The exception handling is also no more or less syntax sugar than the CPS rewrite - it's an error continuation that needs to be routed by querying the underlying Task's properties.
But the really great thing is that you can finally just thread the async keyword through your call stack to get the CPS effect across multiple method call boundaries, even your case of a sorting function. It's not just applicable for a single callback; the second half, the implementation half, is also implemented, so you're not just limited to using the pattern, but creating new instances easily too.
> I don't think the comparison between callbacks and goto is valid.
What is the largest code base you have worked on that used events + callbacks extensively? I worked on a 250,000 lines-of-code Java trading system, and understanding the way the code flow worked from the central event dispatch loop to handler functions, and back to the event dispatch loop took me MONTHS and seriously spoiled the quality of my life (considering how much many hours we spend at work).
Have you used alternative paradigms like Functional Reactive Programming, Promises, etc;? Maybe you don't realize how awesome the alternative is?
for/while are themselves syntactic sugar around goto. You can in fact write them as macros or even functions (using setjmp/longjmp) in C. However, the abstraction in that case will leak - you'll need to put your label manually where the loop needs to go, you'll still need to know you're using goto and how it works and you'll need to keep in mind the exact code that's generated by the macro, or you will get bit by it. Structured looping constructs also don't change the flow in your program and even force another level of indentation, but they allow you to think about it using higher-level building blocks being sure that those blocks always work properly, i.e. they won't break because you have a typo in some conceptually far away line.
Callbacks really are just like goto. I've seen really awful callback code where you have callbacks that create other callbacks which are passed to callback managers which are themselves finite state automatons and call one of the callbacks based on a return value from another. It's the most horrifying spaghetti code you can think of. It's practically fractal - spaghetti within spaghetti that influence the top layer in an untraceable manner.
While everyone can write spaghetti in any language, few can write good code when given just goto or just lambda. In some cases it's even impossible.
There's nothing inherently harmful about goto either, and people who think that just having it around is death, seriously don't understand the machines they're programming, and how we implement these magical control structures they love so much. (hint: we use goto)
I don't feel any respect for the article because it's written on the premise that goto is bad, and that it is anything like a callback.
Callbacks have been, and will continue to be an incredibly useful way to handle events.
If await isn't changing the way you structure your code -- and letting your code robustly handle things that it couldn't handle with callbacks -- I think you're doing it wrong.
Yes a thousand million times. This is the reason why people love golang and why there's a lot of excitement about core.async in the Clojure community, particularly for ClojureScript where we can target the last 11 years of client web browser sans callback hell:
Having spent some time with ClojureScript core.async I believe the CSP model actually has a leg up on task based C# F# style async/await. We can do both Rx style event stream processing and async task coordination under the same conceptual framework.
Yeah, C# await/async is cool. But isn't Google Go's approach nicer? Keep standard lib calls blocking as is, and use the "go" statement when you want to run something async? It avoids all the extra DoWhateverAsync() API functions. See http://stackoverflow.com/a/7480033/68707
What's the Go equivalent of "await"? I.e., like the "go" statement but asynchronously wait for the function to return and get its value?
This sounds sweet. In my book, C# designers have a history of striking a good balance between simplicity and flexibility. This however always leaves me eager to look at Haskell/ClojureScript/other languages where concepts that C# borrowed and simplified are taken to the full (such as monads, iterators, CSP, etc).
I must say that this style of writing async code is much friendlier than descending into callback hell.
There is work to make a similar async interface native in Python 3, in PEP 3156 http://www.python.org/dev/peps/pep-3156/, so this should become more widely available even to those who don't use Twisted.
Async, sure, I'm down with that, but I've used the c# async stuff now, and while it makes it the app somewhat faster, it has three major downsides (that I encountered):
- Infects everything; suddenly your whole application has to be async.
- Debugging becomes a massive headache, because you end up in weird situations where the request has completed before some async operation completes, the debugger gets scared and stops working.
- It's really hard to test properly.
The only good reason for using it is that because of the infection-property back fitting async to your application is a major headache; if you might use it, you have to use it from the beginning or you get a huge backlog of refactoring and test fixes to do.
-___- 'I might use this for some performance bottleneck I don't yet know about, so better start using it now...' yeah, that's a thing: premature optimization.
But doing things by hand with callbacks is going to have all of those same issues isnt it? If you dont want to infect everything then you basically need to just write synchronous code instead...
As someone who only recently switched to Node.js from PHP I personally haven't had any difficulty switching over to the callback frame of mind, and I haven't experienced the "callback hell" so many people complain about. At first I was hesitant to start with Node because I saw blog posts by people bemoaning the spaghetti callback code that they ended up with. But I haven't experienced any of that, although I am a relatively newbie Node programmer with only a few months of experience so far. My current project is quite non trivial as well, running into the tens of thousands of lines so far.
The key I've discovered to nicely organizing callbacks is to avoid anonymous callback functions unless absolutely necessary for a particular scope, or unless the function is going to be so trivial and short that it can be read in a single glance. By passing all longer, non trivial callback functions in by name you can break a task up into clear functional components, and then have all the asynchronous flow magic happen in one concise place where it is easy to determine the flow by looking at the async structure and the function names for each callback function.
Another major advantage to a code organization like this is that once you have your code such that each step has it's own discrete function instead of being some inception style anonymous function tucked away inside another function inside another callback it allows you to properly unit test individual functional steps to ensure that not only is your code working and bug free at the top level functions, but also that each of the individual asynchronous steps that may make up some of your more complicated logic are working properly.
Most of the bad examples of callback hell that I see have anonymous callback functions inside anonymous callback functions, often many levels deep. Of course that is going to be a nightmare to maintain and debug. Callbacks are not the problem though. Badly organized and written code is the problem. Callbacks allow you to write nightmarish code, but they also allow you to write some really beautiful and maintainable code if you use them properly.
I kinda have to disagree with you here. The problem of callback hell has nothing to do with the funcions being anonymous. In fact, you kinda want to have anonymous functions if you want to keep things as similar as possible to traditional code.
For example, when you have code like
var x = f();
print(x);
only a hardcore extremist like Uncle Bob would write it as
var x;
function start(){
x = f();
onAfterF();
}
function onAfterF(){
print(x);
}
because now your code logic is split among a bunch of functions, the variables had to be hoisted to where everyone can see them and the extra functions obscure control flow. In the first case its obvious that its a linear sequence of statements but in the second you cant be sure a-priori how many times onAfterF gets called, when it gets called and who calls it.
Coming back on topic, callback hell is not just about nesting and your current code still suffesr a bit from it. The real problem is that you cant use traditional structured control flow (for, while, try-catch, break, return, etc) and must instead use lots of explicit callbacks. Additionally, for this same reason, callback code looks very different from how you would normally write synchronous code and its a PITA if you ever have to convert a piece of code from one style to the other.
I'm tired to the utmost degree of all these posts about people (supposedly) coming from PHP/C#/Ruby/Python background and seeing "absolutely no problems" with JS syntax, object model and programming paradigms. There are problems. They are objectively there. If you don't see them, you have to check your critical thinking skills, rather than imply that everyone outside of elite JS circles are simply too ignorant to understand its awesomeness.
The simplest example of callback hell is trying to analyze workflow of some chunk of code in a debugger. If the code is linear, you place a breakpoint at the beginning of the method you're interested in and go through the code one line at a time. If there are nested statement of method calls, the debugger happily redirect you to them without fail.
With extensive use of callbacks, this becomes impossible. Since callbacks are merely registered in the original method, you need to place a breakpoint at the beginning of every callback function you might encounter in advance. Named callbacks actually make this worse by physically separating the place where a function is registered from its body. Did I mention that you're loosing ability to do any kinds of static reasoning, since callbacks are inherently a runtime concept? And the fact that you loose ability to look at the stack trace to "reverse engineer" why something was called?
Which reminds me of something. Have you ever seen code that reads a global variable, and you have no clue where the value came from? Callbacks create the exact same problem, except they aren't just data, they are code, so the problem can be nested multiple times.
I often write synchronous methods that include control flow that nests three deep (say try/finally, if/then/else, and a for loop). Often it's easier to read this code than it would be if everything were split out into separate named methods.
Why would the same not be true of asynchronous methods, assuming that the technology was there to enable it (as it is in C#)?
Callbacks have been around forever in C using named functions, and are not specific to either the current generation of programming languages or programmers. One can still use a named function instead of a locally constructed lambda to represent a callback in a high level languages.
The primary difference is that when declaring named functions non-locally, one must explicitly share state through the parameter rather than implicitly sharing state through lexical scoping. It seems more accurate to label the problem of nesting lambdas to the point of ambiguity as "Lambda Abuse" or "Lexical Scoping Abuse" rather than "Callback Hell".
Not totally, the problem being described as "Callback Hell" is in my experience: non-locality of code. Things that relate to one another should be spatially together for a programmer to write, read, understand- and NOT split into success and error conditions around the axis of final callback.
Even with named functions, this is the problem- right? You pass in a named callback and you have to find that function later when you're debugging to figure out what's going on. Locality of code means no breaking context to find something not already on screen and therefore easier programming.
The problem is simply that those languages are not Lisp.
Once good patterns of use of GOTO were found, it was natural to critisize random uses, and to wrap good uses in a lisp macro. Or in a new while or for "instruction".
But then the next construct is discovered, and its bad uses considered harmful, and its good uses need to be wrapped. In lisp, mere programmers will just write the next macro to abstract away this new construct. Other programming languages need to evolve or have new language invented with new "instructions".
So now it's the callbacks. Yes, in lisp we'd just use closures, but this is only a building block for higher level constructs. If those "callbacks" are needed to represent futures, then we'd implement those futures, as a mere lisp macro.
Yes, in the other languages you're still powerless, and need to wait for the language designers to feel the pressure and implement a new "future" instruction or whatever.
Any language construct can be considered harmful eventually. Concretely, a repeative use is a hint of that: the construct becomes meaningless because it's used all the time, or apparently randomly (just like those GOTOs). But it's not that the construct is bad, it's that its usage is not abstracted away in higher level constructs. And the only way to do that is lisp macros.
So unless you're programming in lisp (a homoiconic programming language that let you write easily macros in the language itself), you will always reach a point where some construct will be able to be considered harmful for lack of a way to abstract its uses away.
What if you have two buttons, search and 'be lucky'?
I guess you will have to await for a controller object that wakes up that if either one of the buttons is triggered, and then test what button was pressed.
Now what if you have add a search option dialog that can be invoked at any moment? I guess when the 'apply' button of the option dialog got pressed then this is still going to be a callback that sets global variables according to search option.
So, like any mechanism, this way of structuring code has its limits. If an event can happen at any stage of the flow, then this still has to be handled by a callback, otherwise you would have to check for this kind of event after each await clause.
I agree, although I think callbacks are more like COME FROM than goto. You see a function being passed somewhere as a callback, and you know the block is going to execute at some point, but most of the time you have no idea what the codepath that calls you back looks like.
There's nothing more frustrating than trying to debug why a callback isn't being called. Who calls it? How do I set a breakpoint somewhere to see why it isn't being called? etc.
The one thing that is still missing from await and other green thread approaches is cheap global contexts. Isolating every different green thread so they can't implicitly share state is the obvious next step.
Have you looked at the Erlang model? Each Erlang process (green thread) only gets the arguments initially passed in and whatever else it asks for from other running processes. The only shared state is long-running processes created for the purpose of explicitly sharing state.
I generally agree that there are better ways to handle asynchronous control flow than callbacks, but I think this is exaggerated. As in most posts like this, the callback soup examples are difficult to follow primarily because they are horribly written, not because of callbacks.
As long as you write decent code, the main impediment to asynchronous programming is reasoning asynchronously, not syntax. If you require complex asynchronous logic and don't use an appropriate algorithm, you'll end up in the muck whether you use callbacks or await.
Taking go as an example: while I agree that the go statement is more elegant than a callback approach, I see it as quite a minor win compared to channels. The go statement is convenient syntax, but channels are what make concurrency in go feel so robust, and it's a pattern than can be applied just as well in a language that uses callbacks.
Saying that, I don't mind the way things look with the whole await/async stuff in C# and etc. However I don't think we should be waving our arms around saying callbacks are like goto, they so completely are not! I have written heaps of stuff with callbacks and it's _not that confusing or unmaintainable_. It's just different.
foreach (var player in players) {
while (true) {
var name = await Ask("What's your name");
if (IsValidName(name)) {
player.name = name;
break;
}
}
}
Assuming `Ask` is an asynchronous operation and must not block the UI thread.
Note that second player is only asked after the first player has given a valid name.
(And the code structure reflects that :-)
My point is of course it's doable with callbacks, but I spent more time indenting this code than writing it, and I darn well know I'm not smart enough to spell out the correct callback-style code in a comment field on Hacker News. And if I suddenly had to add error handling...
Why do you as an American feel the need to invoke some dead german left-wing militants in a pseudo-religious phrase that's meaningless except maybe for shock value? This seems highly inapproriate for any website and even more so on HN.
I'm sick of these app developers assuming that using "goto" is bad practice. The fact is that "goto" is used plenty in great production code you're probably running right now.[1] I'd like to know a cleaner way to abort a function into its cleanup phase when a function call returns an error. And "goto" statements are extremely simple to handle for even the most naive of compilers.
Evan Czaplicki (author of Elm lang) made the identical argument (sometime?/years ago), with the same reference to Dijkstra's quote, but with another suggested solution, Functional Reactive Programming, on which his language is oriented:
FRP is an interesting topic (I thought so anyway, I wrote a paper on it for my MS). It doesn't seem to have caught on widely as a paradigm, with a few exceptions I'm aware of (Elm, Meteor).
Anonymous callbacks are very powerful and very important. They will make you feel bad for unnecessary nesting. They will force you to learn how to abstract better, especially state changes. They will show you how nice and reliable code can be if it doesn't have shared states across multiple functions and how easy it is to understand consistent code with explicit continuations and how to write one yourself. They will make you a better programmer.
And "await" can only make it harder to visually distinguish which piece of code is executed in parallel and which is executed sequentially. Nesting makes it explicit.
Why do people insist on analysing things using analogies? Analogies are useful for explaining a concept that might not be obvious. Saying callbacks are like gotos, gotos are bad, therefore callbacks are bad is ridiculous.
And he gives some sample code where the 'problem' is nothing to do with callbacks, its just nested lambdas. In fact I find that code quite easy to read, and would be very interested in seeing the same functionality implemented some other way, bearing in mind it is quite a difficult problem to synchronize multiple async operations and usually requires horrible code using multiple mutex.
I'm confused as to why you think the code presented in the blog post isn't an example of 'the same functionality implemented some other way'. The await code is almost undeniably more straightforward and the exceptional cases more obvious to handle.
He also doesn't seem to be making a weird logical leap the way you claim he is. He's not really using an analogy. He's saying callbacks are bad the same way goto is bad, in that they make the logical structure of a program's execution non-obvious, particularly over time and when being modified.
This definitely is a problem in Obj-C. Using GCD and callbacks is usually easier to understand than delegates and manual thread management, but it's still not great. I would love to see something like async/await in Obj-C. There are some great ideas on how to get something similar in this blogpost, but none that I would use in production code unfortunately:
http://overooped.com/post/41803252527/methods-of-concurrency
It's right that callback model sucks, and the task model is a way to go.
Sadly, many developers when they hear the word "C# async"
...
All of these statements are made by people that have yet
to study C# async or to grasp what it does.
But it's unpleasant to see the author is talking concept of task - lightweight threading, coroutine, or whatever - is like a patent of C# (or F#). And furthermore, treating many developers are not able to understand this concept.
Maybe true for the people around him.
I understand his position as a lead developer and an evangelist of Mono/C#, but this attitude is ridiculous.
[+] [-] crazygringo|12 years ago|reply
That being said, I don't think the comparison between callbacks and goto is valid.
"Goto" allows you to create horrible spaghetti-code programs, and getting rid of it forces you to structure your programs better.
"Await", fundamentally, isn't really anything more than syntactic sugar (except for exception handling, which is a good thing). "Await" doesn't change how your program is structured at all, it just changes the visual representation of your code -- from indentations in a non-linear order, to vertical and linear order. It's definitely a nice improvement, and makes code easier to understand (and allows for better exception handling), but it's not actually changing the way your program is fundamentally structured.
And finally, "await" is only applicable when a single callback gets called once at the end. If you're passing a callback that gets used repeatedly (a sorting function, for example), then normal-style callbacks are still necessary, and not harmful at all. Sometimes they can be short lambdas, sometimes they're necessarily much larger.
In sum: "await" is great, but there's nothing inherently harmful about callbacks, the way "goto" is. To the contrary -- callbacks are amazingly useful, and amazingly powerful in languages like JavaScript. "Await" just makes them nicer.
[+] [-] blahedo|12 years ago|reply
Taking the OP's premise at face value, then, if callbacks are like goto, then await takes away one place where they were needed and replaces them with something more structured and safer---and this doesn't at all negate the possibility that there are other constructs yet to be invented that would continue that process.
[+] [-] danabramov|12 years ago|reply
Await does change how your program is structured, unless we're talking about most trivial cases.
You can use `await` inside a `for` loop—can you do the same with callbacks without significantly re-structuring your code?
What is the “callback analog” of placing something in a `finally` block that executes no matter which callback in a nested chain fails? You'd have to repeat that code.
Await has a potential of simplifying the structure a lot, because it befriends asynchronous operations with control flow.
>And finally, "await" is only applicable when a single callback gets called once at the end. If you're passing a callback that gets used repeatedly (a sorting function, for example), then normal-style callbacks are still necessary, and not harmful at all.
Indeed, await is only for the cases when we abuse functions (because we don't know what we'll call, and we have to bend our minds). Passing the sorting comparator is the primary use of first-class functions.
[+] [-] wrl|12 years ago|reply
[+] [-] marshray|12 years ago|reply
As a C++ guy, I don't like his "Busy = false" system to begin with. It would seem much better (to me) if he used a non-copyable object to represent the outstanding activity. Such an object could naturally reset the "Busy" flag in its destructor. But usually there's a better scheme than using a simple Boolean flag to represent a "busy" state anyway. (How is clearing the flag going to release the next guy waiting for the resource?)
So while I'm still a bit skeptical of drawing conclusions from this, I readily admit it's not so off-the-wall as I'd originally thought.
[+] [-] barrkel|12 years ago|reply
http://blog.barrkel.com/2006/07/fun-with-asynchronous-method...
Await is basically a continuation-passing style transform that puts the continuation in the continue handler of the task. The exception handling is also no more or less syntax sugar than the CPS rewrite - it's an error continuation that needs to be routed by querying the underlying Task's properties.
But the really great thing is that you can finally just thread the async keyword through your call stack to get the CPS effect across multiple method call boundaries, even your case of a sorting function. It's not just applicable for a single callback; the second half, the implementation half, is also implemented, so you're not just limited to using the pattern, but creating new instances easily too.
[+] [-] vijucat|12 years ago|reply
What is the largest code base you have worked on that used events + callbacks extensively? I worked on a 250,000 lines-of-code Java trading system, and understanding the way the code flow worked from the central event dispatch loop to handler functions, and back to the event dispatch loop took me MONTHS and seriously spoiled the quality of my life (considering how much many hours we spend at work).
Have you used alternative paradigms like Functional Reactive Programming, Promises, etc;? Maybe you don't realize how awesome the alternative is?
[+] [-] anonymous|12 years ago|reply
Callbacks really are just like goto. I've seen really awful callback code where you have callbacks that create other callbacks which are passed to callback managers which are themselves finite state automatons and call one of the callbacks based on a return value from another. It's the most horrifying spaghetti code you can think of. It's practically fractal - spaghetti within spaghetti that influence the top layer in an untraceable manner.
While everyone can write spaghetti in any language, few can write good code when given just goto or just lambda. In some cases it's even impossible.
[+] [-] microcolonel|12 years ago|reply
I don't feel any respect for the article because it's written on the premise that goto is bad, and that it is anything like a callback.
Callbacks have been, and will continue to be an incredibly useful way to handle events.
[+] [-] JoshGlazebrook|12 years ago|reply
https://github.com/bjouhier/galaxy
Essentially await/async using Harmony Generators.
[+] [-] dustingetz|12 years ago|reply
[+] [-] swannodette|12 years ago|reply
http://swannodette.github.io/2013/07/12/communicating-sequen...
Having spent some time with ClojureScript core.async I believe the CSP model actually has a leg up on task based C# F# style async/await. We can do both Rx style event stream processing and async task coordination under the same conceptual framework.
[+] [-] benhoyt|12 years ago|reply
What's the Go equivalent of "await"? I.e., like the "go" statement but asynchronously wait for the function to return and get its value?
[+] [-] danabramov|12 years ago|reply
[+] [-] lambda|12 years ago|reply
There is work to make a similar async interface native in Python 3, in PEP 3156 http://www.python.org/dev/peps/pep-3156/, so this should become more widely available even to those who don't use Twisted.
[+] [-] rosh|12 years ago|reply
[+] [-] shadowmint|12 years ago|reply
Async, sure, I'm down with that, but I've used the c# async stuff now, and while it makes it the app somewhat faster, it has three major downsides (that I encountered):
- Infects everything; suddenly your whole application has to be async.
- Debugging becomes a massive headache, because you end up in weird situations where the request has completed before some async operation completes, the debugger gets scared and stops working.
- It's really hard to test properly.
The only good reason for using it is that because of the infection-property back fitting async to your application is a major headache; if you might use it, you have to use it from the beginning or you get a huge backlog of refactoring and test fixes to do.
-___- 'I might use this for some performance bottleneck I don't yet know about, so better start using it now...' yeah, that's a thing: premature optimization.
[+] [-] ufo|12 years ago|reply
[+] [-] localhost|12 years ago|reply
[+] [-] NathanKP|12 years ago|reply
The key I've discovered to nicely organizing callbacks is to avoid anonymous callback functions unless absolutely necessary for a particular scope, or unless the function is going to be so trivial and short that it can be read in a single glance. By passing all longer, non trivial callback functions in by name you can break a task up into clear functional components, and then have all the asynchronous flow magic happen in one concise place where it is easy to determine the flow by looking at the async structure and the function names for each callback function.
Another major advantage to a code organization like this is that once you have your code such that each step has it's own discrete function instead of being some inception style anonymous function tucked away inside another function inside another callback it allows you to properly unit test individual functional steps to ensure that not only is your code working and bug free at the top level functions, but also that each of the individual asynchronous steps that may make up some of your more complicated logic are working properly.
Most of the bad examples of callback hell that I see have anonymous callback functions inside anonymous callback functions, often many levels deep. Of course that is going to be a nightmare to maintain and debug. Callbacks are not the problem though. Badly organized and written code is the problem. Callbacks allow you to write nightmarish code, but they also allow you to write some really beautiful and maintainable code if you use them properly.
[+] [-] ufo|12 years ago|reply
For example, when you have code like
only a hardcore extremist like Uncle Bob would write it as because now your code logic is split among a bunch of functions, the variables had to be hoisted to where everyone can see them and the extra functions obscure control flow. In the first case its obvious that its a linear sequence of statements but in the second you cant be sure a-priori how many times onAfterF gets called, when it gets called and who calls it.Coming back on topic, callback hell is not just about nesting and your current code still suffesr a bit from it. The real problem is that you cant use traditional structured control flow (for, while, try-catch, break, return, etc) and must instead use lots of explicit callbacks. Additionally, for this same reason, callback code looks very different from how you would normally write synchronous code and its a PITA if you ever have to convert a piece of code from one style to the other.
[+] [-] gambler|12 years ago|reply
The simplest example of callback hell is trying to analyze workflow of some chunk of code in a debugger. If the code is linear, you place a breakpoint at the beginning of the method you're interested in and go through the code one line at a time. If there are nested statement of method calls, the debugger happily redirect you to them without fail.
With extensive use of callbacks, this becomes impossible. Since callbacks are merely registered in the original method, you need to place a breakpoint at the beginning of every callback function you might encounter in advance. Named callbacks actually make this worse by physically separating the place where a function is registered from its body. Did I mention that you're loosing ability to do any kinds of static reasoning, since callbacks are inherently a runtime concept? And the fact that you loose ability to look at the stack trace to "reverse engineer" why something was called?
Which reminds me of something. Have you ever seen code that reads a global variable, and you have no clue where the value came from? Callbacks create the exact same problem, except they aren't just data, they are code, so the problem can be nested multiple times.
[+] [-] kvb|12 years ago|reply
Why would the same not be true of asynchronous methods, assuming that the technology was there to enable it (as it is in C#)?
[+] [-] davidhollander|12 years ago|reply
Callbacks have been around forever in C using named functions, and are not specific to either the current generation of programming languages or programmers. One can still use a named function instead of a locally constructed lambda to represent a callback in a high level languages.
The primary difference is that when declaring named functions non-locally, one must explicitly share state through the parameter rather than implicitly sharing state through lexical scoping. It seems more accurate to label the problem of nesting lambdas to the point of ambiguity as "Lambda Abuse" or "Lexical Scoping Abuse" rather than "Callback Hell".
[+] [-] haimez|12 years ago|reply
Even with named functions, this is the problem- right? You pass in a named callback and you have to find that function later when you're debugging to figure out what's going on. Locality of code means no breaking context to find something not already on screen and therefore easier programming.
[+] [-] informatimago|12 years ago|reply
Once good patterns of use of GOTO were found, it was natural to critisize random uses, and to wrap good uses in a lisp macro. Or in a new while or for "instruction".
But then the next construct is discovered, and its bad uses considered harmful, and its good uses need to be wrapped. In lisp, mere programmers will just write the next macro to abstract away this new construct. Other programming languages need to evolve or have new language invented with new "instructions".
So now it's the callbacks. Yes, in lisp we'd just use closures, but this is only a building block for higher level constructs. If those "callbacks" are needed to represent futures, then we'd implement those futures, as a mere lisp macro.
Yes, in the other languages you're still powerless, and need to wait for the language designers to feel the pressure and implement a new "future" instruction or whatever.
Any language construct can be considered harmful eventually. Concretely, a repeative use is a hint of that: the construct becomes meaningless because it's used all the time, or apparently randomly (just like those GOTOs). But it's not that the construct is bad, it's that its usage is not abstracted away in higher level constructs. And the only way to do that is lisp macros.
So unless you're programming in lisp (a homoiconic programming language that let you write easily macros in the language itself), you will always reach a point where some construct will be able to be considered harmful for lack of a way to abstract its uses away.
[+] [-] danabramov|12 years ago|reply
http://praeclarum.org/post/45277337108/await-in-the-land-of-...
Basically, the author implements a “first time walkthrough” kind of interface a-la iWork very declaratively by using async:
[+] [-] MichaelMoser123|12 years ago|reply
Now what if you have add a search option dialog that can be invoked at any moment? I guess when the 'apply' button of the option dialog got pressed then this is still going to be a callback that sets global variables according to search option.
So, like any mechanism, this way of structuring code has its limits. If an event can happen at any stage of the flow, then this still has to be handled by a callback, otherwise you would have to check for this kind of event after each await clause.
[+] [-] fzzzy|12 years ago|reply
There's nothing more frustrating than trying to debug why a callback isn't being called. Who calls it? How do I set a breakpoint somewhere to see why it isn't being called? etc.
The one thing that is still missing from await and other green thread approaches is cheap global contexts. Isolating every different green thread so they can't implicitly share state is the obvious next step.
[+] [-] nickmeharry|12 years ago|reply
[+] [-] makeset|12 years ago|reply
[+] [-] danenania|12 years ago|reply
As long as you write decent code, the main impediment to asynchronous programming is reasoning asynchronously, not syntax. If you require complex asynchronous logic and don't use an appropriate algorithm, you'll end up in the muck whether you use callbacks or await.
Taking go as an example: while I agree that the go statement is more elegant than a callback approach, I see it as quite a minor win compared to channels. The go statement is convenient syntax, but channels are what make concurrency in go feel so robust, and it's a pattern than can be applied just as well in a language that uses callbacks.
[+] [-] ryan-allen|12 years ago|reply
Callbacks 'get crazy' when you've got more than one I think, and thankfully someone smart has made a library you can use to manage them!
https://github.com/caolan/async
Saying that, I don't mind the way things look with the whole await/async stuff in C# and etc. However I don't think we should be waving our arms around saying callbacks are like goto, they so completely are not! I have written heaps of stuff with callbacks and it's _not that confusing or unmaintainable_. It's just different.
[+] [-] danabramov|12 years ago|reply
Note that second player is only asked after the first player has given a valid name.
(And the code structure reflects that :-)
My point is of course it's doable with callbacks, but I spent more time indenting this code than writing it, and I darn well know I'm not smart enough to spell out the correct callback-style code in a comment field on Hacker News. And if I suddenly had to add error handling...
[+] [-] cscheid|12 years ago|reply
https://gist.github.com/cscheid/6241817
[+] [-] nkuttler|12 years ago|reply
[+] [-] dantle|12 years ago|reply
I'm sick of these app developers assuming that using "goto" is bad practice. The fact is that "goto" is used plenty in great production code you're probably running right now.[1] I'd like to know a cleaner way to abort a function into its cleanup phase when a function call returns an error. And "goto" statements are extremely simple to handle for even the most naive of compilers.
[1] https://www.kernel.org/doc/Documentation/CodingStyle (see chapter 7)
[+] [-] pjmlp|12 years ago|reply
[+] [-] worldsayshi|12 years ago|reply
http://elm-lang.org/learn/Escape-from-Callback-Hell.elm
[+] [-] jcurbo|12 years ago|reply
FRP is an interesting topic (I thought so anyway, I wrote a paper on it for my MS). It doesn't seem to have caught on widely as a paradigm, with a few exceptions I'm aware of (Elm, Meteor).
[+] [-] solistice|12 years ago|reply
[+] [-] zzzcpan|12 years ago|reply
And "await" can only make it harder to visually distinguish which piece of code is executed in parallel and which is executed sequentially. Nesting makes it explicit.
[+] [-] adamconroy|12 years ago|reply
And he gives some sample code where the 'problem' is nothing to do with callbacks, its just nested lambdas. In fact I find that code quite easy to read, and would be very interested in seeing the same functionality implemented some other way, bearing in mind it is quite a difficult problem to synchronize multiple async operations and usually requires horrible code using multiple mutex.
[+] [-] zowch|12 years ago|reply
He also doesn't seem to be making a weird logical leap the way you claim he is. He's not really using an analogy. He's saying callbacks are bad the same way goto is bad, in that they make the logical structure of a program's execution non-obvious, particularly over time and when being modified.
[+] [-] cbrauchli|12 years ago|reply
[+] [-] eonil|12 years ago|reply
Maybe true for the people around him.
I understand his position as a lead developer and an evangelist of Mono/C#, but this attitude is ridiculous.
[+] [-] danabramov|12 years ago|reply
[+] [-] pfraze|12 years ago|reply