17 ms·
Callbacks are Pretty Okay
- wingspan 13y agoI never thought of using nested functions like that. It does make the code much more readable, and using 100% JS is a big win. Not as pretty as yield/await, but much more debuggable and compatible.
- a__remie 13y agoNesting is not the problem, you see what you're doing (when you perform an async io request). And you still have the "if (e) return cb(e)" line... And on top of that, it does not look better.
- aa0 13y agoAn operation queue where dependencies can be set is also very relevant. For example, if you have to do 2 different json requests, followed by a render - but they are mutually exclusive, you can do a 2->1 dependency. If they do depend on each other's result, you can do a 1->1->1 I like this async syntax but I'm more fond of native JS generator trampoline. I think the JSOperation with dependencies could even use generators in its implementation. All these solutions coming about after the C# async article are very enlightening to this problem of the synchronic vs diachronic vs asynchronic. The JSOperations lib is in alpha but can be found here https://github.com/iandrewfuchs/JSOperations/blob/master/README.md https://github.com/iandrewfuchs/JSOperations/blob/master/REA...
- rob05c 13y agoI have a project in Go which is pretty heavily functional. Over time, I've begun to notice the general goodness of his 2 rules. If you didn't RTFA, they were 1. Avoid nontrivial anonymous functions. 2. Function declarations after the code that actually does things. I wonder if his advice is good for all functional-style code in non-functional languages.
- prezjordan 13y agoRight, but now your functions are very tightly coupled to each other. The indentation might be nicer, but functionally it's just misleading. The cool thing about promises is that your methods are pretty portable. For example: function getUserFriends(userName, next) { db.users.findOne({name:userName}, foundOne); function foundOne(err, user) { if (err != null) return next(err); db.friends.find({userId:user.id}, foundFriends); } function foundFriends(err, friends) { if (err != null) return next(err); return next(null, friends); } } Can be rewritten (using q[0]) as: var Q = require('q'); function getUserFriends(userName, next) { var deferred = Q.defer(); db.users.findOne({name:userName}, deferred.makeNodeResolver()); return deferred.promise; } function foundOne(user) { var deferred = Q.defer(); db.friends.find({userId:user.id}, deferred.makeNodeResolver()); return deferred.promise; } function foundFriends(friends) { ... } getUserFriends.then(foundOne).then(foundFriends); You'll see here that getUserFriends, foundOne, and foundFriends (which I left out) are now all portable and can be used elsewhere in the code for other purposes (because they are no longer tightly coupled). [0]: https://github.com/kriskowal/q https://github.com/kriskowal/q
- StavrosK 13y agoWith the downside that I have absolutely no clue what this code does or how.
- DrJokepu 13y agoOnce you get used to promises (it took me a couple of days) it's not confusing at all. It takes a bit of effort though. I remember when I first started programming none of the programs made any sense to me; as you said, I had absolutely no clue what the code did and how. But then I kept practicing and soon I understood how things work.
- StavrosK 13y agoYeah, I guess it's not fair for me to compare something I'm familiar with to something I'm not. I'm sure promises are easy to understand, but that API there does not look great.
- modiho 13y agoQ has some utility functions around wrapping node style callbacks that make the code a bit shorter and arguably more easy to understand. var Q = require('q'); function getUserFriends(userName) { return Q.nfcall(db.users.findOne, {name:userName}); } function foundOne(user) { return Q.nfcall(db.friends.find, {userId:user.id}); } function foundFriends(friends) { ... } getUserFriends.then(foundOne).then(foundFriends);
- prezjordan 13y agoThat's much better, thanks for sharing
- gruseom 13y agoI am wary of designs that make code significantly longer, as both these examples do. The improvement in local readability is often superficial and the cost in overall readability (not to mention bugs, which increase with code size) is often nontrivial. In the examples, each line of code with a named function appears more readable, but this is only because its meaning has become less dense—and as there is also now more code to read, the two factors arguably cancel each other out. Meanwhile the new code has more non-locality: the eye of the reader must jump between the various function invocations and declarations. That's a lot of jumping, and it will get much worse as complexity grows. Introducing a name has costs as well as benefits. For example, it adds indirection: the name now stands between the reader and the thing that is named. To overcome this, a name should say something about the problem the code is solving. If all it says is something about the code itself, that's not good—it adds weight and obscures the problem (because you're now taking more code to solve it). It feels like you're simplifying code by factoring it in this way, but you may only be spreading it out more, making pieces which are logically bound together further apart from each other. A sign that this is happening is when you can't think of a good name for the code you are factoring, or when the name refers only to the code itself. "wtfFriendAction" is a comical example, but I think "gotFriends" is more instructive: it tells you nothing more than "I am the callback for getFriends", and thus adds no information to the original code, only verbosity.
- jay_kyburz 13y agoI don't think naming your functions has to make the code "significantly longer". Perhaps 2 lines per callback. And yes it can be hard to name functions, and I think the OP has not created very good example, but a good name that accurately describes what the function does means I don't have to read it if I don't want to. A better example would have been some node.js code that has 5 levels of nesting, each with a some significant code within. You've just got to name them.
- gruseom 13y agoThe examples are 40% and 20% longer. That is significant. The trouble with small examples is that their problems don't seem like a big deal because their entire codebase isn't a big deal. You have to apply them many times over to get any sense of their compounding effect, which is what really matters. But by definition, that's out of scope of a discussion whose examples are small. The only way out of this paradox is to pay close attention to apparently small effects.
- hosay123 13y agoThis recent 'callbacks as goto' discussion is so utterly mundane that I've all but failed to convince my wrists to so much as twitch sufficiently to click on any of the links, just the title is enough to drive me to drink. Callbacks are in no way "this generation's goto", they do not in any way inhibit the ability to analyse or prove correct a program, and all alternatives to callbacks amount to incredibly fancy syntax sugar for hiding what is really happening behind the scenes anyway. A callback is a function call, or put another way, a specification of an exact set of arguments grouped in a formal specification that is sent to a deterministic little machine (the function) to execute immediately. On completion the machine will provably produce the result it promised to return in its owner's manual (i.e. the declared return code). None of this is "goto" in any way. In goto land, without knowing the exact set of inputs a program receives and whole-system simulation, it is utterly impossible to make assumptions about even the most minimal pieces of state in the program at any given moment. Contrast to a function call. A function call is a fixed guarantee about the state of the system at the point a machine begins executing. Furthermore a function call has a vastly restricted set of valid behaviours: at some point it must terminate, and prior to termination, update the system state (stack) to include its return value. And upon termination, result in the calling machine to resume operation. All this syntax sugar shit is whack. Great, you typed 24 characters instead of 3 lines to declare a lambda. Hyper productive, go you. How does this progress toward the point where I can say "computer, make me a coffee!"? If you're genuinely interested in what this generation's Goto might look like, take 30 minutes to watch http://vimeo.com/71278954 http://vimeo.com/71278954 .. our UIs are utterly trapped in pre-1970s thinking, our communication networks are so utterly idiotic that we STILL have to write custom code to link disparate chunks of data and logic together, we're STILL writing goddamned function calls in a freaking text editor (something that was solved LONG ago). All the things like this. I can't ask my computer to make me a cup of coffee, and it responds. I can't describe in restricted English a simple query problem and have the millions of idle machines around the world coordinate meaningfully to answer my question (and our best chances of this.. freebase.com.. got bought up by Google and left to rot as some vestigial appendage of their antispam dept no doubt). Computing is in a sad state today, but not because of fucking callbacks. It's in a sad state today because we're still thinking about problems on this level at all. Node.js wasn't an innovation. It was shit that was solved 60 years ago. I wish more people would understand this. Nothing improves if we all start typing 'await' instead of defining callbacks. Innovation in our industry has been glacial since at least the early 90s. Edit: and as a final note, I wholeheartedly welcome you to nitpick the hell out my rant, wax verbose on the definitions of provability, the concept of a stack, the use of English versus American language, why Vim is awesome, and in the process once more demonstrate the amoeba-like mentality 90% of our industry is trapped in. Go and build a programming language your Mom can use. Preferably by talking to her computer. Please, just don't waste it on any more of this insanity.
- deleted 13y ago[deleted]
- Gonzih 13y agoNo, they are not.
- chapel 13y agoThis is essentially how I have been writing JavaScript and Node.js code for some time. I actually flatten the code even more, like in these examples[0]. For some people, callbacks take more cognition than other solutions, but for someone who has been using callbacks through and through, this is a natural progression and makes sense. It is more straightforward and you don't have to understand further abstractions like in a compile to JS language or with promises. [0]: https://gist.github.com/chapel/6255720 https://gist.github.com/chapel/6255720
- antihero 13y agoI think I've done pretty much exactly the same thing: https://gist.github.com/radiosilence/2fd82aaf3b1721448ae1 https://gist.github.com/radiosilence/2fd82aaf3b1721448ae1 Except your function names make more sense.
- chmike 13y agoThe real problem with callbacks is error handling. Using anonymous functions doesn't address this problem. The .then notation has the same problem. The await solution (or any equivalent) has the benefit to present the call as a synchronous call which is trivial to read and understand. It also has the benefit to support all existing and proven error handling methods. try catch, etc. This is the best solution to asynchronous calls that I have seen so far. I'm currently using c++ asio, and it's a nightmare.
- lucian1900 13y agoPromises are actually pretty good at error handling, the first error encountered is returned.
- Nitramp 13y agoWell, Q allows you to attach an error handler with catch: bla(). then(something). then(someOther). catch(ohNoes); First, this gives you really bad stack traces, essentially you have no chance to figure out what happened before your error handler got called. You can get slightly less bizarre stack traces with `Q.longStackSupport = true;`, but that has a serious performance impact (creating a new throwable per invocation). Second, if you try to get better code structure by breaking up your program into named functions ('something', 'someOther' above), you loose locality, i.e. your error handler does not share a scope with its causing code. You can fix that by passing the entire promise to a processing function, but this again obscures things. Compared to a program that just does: x := bla(); var y; try { y = something(x); } catch(...) { ... } return someOther(y); The promise code is still a lot more complicated. I agree with Miguel de Icaza, continuation passing style/callbacks do destroy 'natural' program structure, and make it much harder to use the regular programming language's idioms (loops, returns, ...). I'd be hugely in favour of go's approach with very cheap goroutines, so you don't actually have to use CPS just because your runtime isn't clever enough to use more than one thread (ahem node). OTOH go's lack of exceptions kills the whole argument by bringing back the error handling of the 80s :-(
- casperc 13y agoI guess it comes down to preferences in readability. Personally I agree that not putting too much logic in anonymous functions makes it more readable. I write my own code that way, but some might disagree. I don't care about whether functions are above or below where they are used, as long as it is consistent - but event then it is a minor thing. And since it isn't enforced by the language (e.g. like in Clojure where you have to def the var/function above the reference) it is going to vary across code bases anyway. I don't really think either solves callbacks, nor that callbacks is a problem to be solved. Most comes down to pros and cons seen through the filter of your personal preferences. Sure you can use a language that compiles to javascript. Does the pros outweigh the cons for you? Great, use it/too bad move right along. Not everything is right or wrong, good or bad - in fact most things aren't.
- ulisesrmzroche 13y agoThing is that it actually matters in JS because declaring a function this way triggers the hoisting mechanism, so named functions are in scope within the entire function in which it is declared, regardless of block nesting. If one isn't careful, and because JS doesn't have the visual delineator of blocks, instead all these rules you have to memorize, this could easily could lead to bugs, especially in large code-bases. I think it's good to rely on closures to keep from polluting your scopes and to not use uneccesary function names, which is going to lead to one giant imperative statement (the one with the sequence of commands for the computer to perform) rather than a lot of little functions that you can pass around. Secrets of the JS ninja goes really in depth about this.
- esailija 13y agoThere is no such thing as an actual hoisting mechanism. Yes, declared functions are available anywhere in the surrounding scope, just like in any other language. It would be ridiculous if for example you couldn't call a method in Java inside another method unless it was syntactically placed before the target. Global scope pollution is completely irrelevant here - declarations are available in the surrounding scope, not global. If the surrounding scope is global (which it never is in node.js), then that's your problem.
- rickygw 13y agoThanks for this OP. I use these 2 rules in my every day JS because where I work, readability wins every time. The anon function / indentation code format is something I've never really tried to "pick up" when I migrated over to javascript. Its ugly and takes longer for someone outside of my head to figure out what is going on in my code. Verbose function names help the other 4 guys on my team know exactly which function is handling the different scenarios of an async call. Its much more important that the developers are able to work efficiently than it is to save a few characters per function or a few lines per callback. With the state of JS compilers/minifiers and gzip, I really feel that that particular argument can't even be made in most JS situations.
- zhyder 13y agoYou don't need any nesting. All those methods could be declared at the same level, though that entails passing more params to them since you can't rely on closures. That's probably how you'd divide your synchronous code into methods anyway, so you might as well do the same with your async code.
- hijackson 13y agoI'm creating $86 associate degree hour engaging from home. i used to be appalled once my neighbour told Pine Tree State she was averaging $95 however I see however it works currently. I feel most freedom currently that i am my very own boss. this is often what I do, www.jobs34.com
- antihero 13y ago"Coffee-Script actually prohibits this kind of code organization, because all functions are necessarily assignments" How about this then? I haven't tested. Perhaps functions need to be in a different order, but we only have one level of nesting: https://gist.github.com/radiosilence/2fd82aaf3b1721448ae1 https://gist.github.com/radiosilence/2fd82aaf3b1721448ae1
- deleted 13y ago[deleted]
- sklivvz1971 13y agoI think callbacks are still pretty broken even after applying the "two rules". Yes, one avoids nesting, but you still break your procedure/function/method into chunks which make no sense by themselves. I don't see how that can lead to good code. Better than a million nested callbacks? Sure. Readable? Nope. How do you even name all those callbacks?
- eonil 13y agoThis seems misled response to misled article.
- nickjackson 13y agoI agree with the OP. Its all about how you structure and name your code to avoid deeply nested callbacks. With the use of Prototype constructs and decent directory structures you can keep code clean and concise and completely avoid callback hell. Take a look at the following users repos to see exactly how clean javascript code can be. https://github.com/learnboost https://github.com/learnboost https://github.com/component https://github.com/component https://github.com/visionmedia https://github.com/visionmedia
- ulisesrmzroche 13y agoI think this is a step backward toward procedural programming (needing to name each function, ignoring the hoisting mechanism that happens when you declare a function that way) and then took another step back by putting it all in one huge imperative statement at the top). Probably going to lead to bugs if one isn't careful with the function scope. The best part about JS is that you can be use a functional, declarative style, which is awesome.
- undoware 13y agoThis dude fails the understanding-LiveScript test. Next time you bash something, read up about it, okay? 'backcalls' are purely syntactical -- they're callbacks, but with a special syntax so you don't have to indent.
- AndyKelley 13y agoThat's what I thought, but someone claimed otherwise: https://news.ycombinator.com/item?id=6227958 https://news.ycombinator.com/item?id=6227958 Instead of investigating and finding out for sure, I communicated my incomplete knowledge without context. You're right - I should have investigated and then communicated the correct knowledge. Any LiveScript bashing was unintentional.
- jameshart 13y agoHow can you make this argument and not even mention the word 'closure'? That's like arguing there's no need for object orientation, and demonstrating it by showing how you can model any class as just a set of global functions, by only showing classes which have no instance state. Replacing anonymous functions with named functions works fine unless the anonymous functions are closures. If they are, you're going to have to add parameters to them to pass state in, and you're in real trouble if you want to have them modify closed state, because in JavaScript you've got no pass-by-reference. But closures that modify closed variables are hard to reason about in the usual nested callback syntax model. What the async 'syntactic sugar' approaches do is replace nested closure scopes with simple control-structure scoping that's easy to reason about - even if the 'callbacks' are modifying state which then gets made available to subsequent callbacks.
- zemo 13y agoIt looks like you opened the article, hit "ctrl+f closure", saw 0, and went on a rant. In the examples where the turned an anonymous function into a named function, the named functions still close over symbols from their calling context, so... it's hard to see what you're on about.
- jameshart 13y agoHis examples only close over parameter variables and lifted functions, not local variables that actually, you know, vary. The fact is, in JavaScript, lifted functions can close over local variables but the fact that they do is not obvious - in fact it's so counterintuitive that you're generally best to avoid doing so. In the first example he gives, two functions that, in the original, are nested, in the refactoring are placed in the same scope, rather than nested within one another. His amended solution prevents the second function from closing over the scope of the first, which is possible in the original, or an async form. If you wanted to share state from the first function to the second, you'd have to realize you could bring the second function into the scope of the first, and then move it, and then you're back where you started with nested callbacks. But amending this code is made harder than it should be by the decision to factor the two closures out to the widest possible scope, rather than the narrowest. And the fact that ctrl-F closure yields no results just confirms the feeling that the examples gave me that the author is playing around with closures without knowing what he's actually doing.
- AYBABTME 13y agoThe main problem I see with callbacks is that people forget that deeply nested structures are too cumbersome to hold in one's brains. The OP rules to break down things in smaller, named function is a solution to a more general problem: don't nest things if you can avoid to. Callbacks aren't the problem. People using them are the problem. Keep your logic as flat as possible and callback-hell will dissolves before your eyes. Programming dogma is so annoying.
- coldtea 13y ago"Callbacks are Pretty Okay" translation: I'm a programmer in my twenties, work mostly in Javascript, have no much experience with the historic literature and core ideas of Compute Science, don't know any better, and I like node, so there.
- coldtea 13y ago>In this refactored code, we've only reduced the maximum nesting by 1 - from 8 to 7. But consider how much easier it is to follow. Are you fucking kidding me?
- pacala 13y agocallbacks are also known as continuations which are known as generalized goto.
- tkiley 13y agoI like Andrew's rules here, and reading over one of my recent node.js projects, I can see how they would have helped. My code becomes cleaner and more maintainable when I am forced to name each chunk of 5-10 lines by putting them into separate functions/methods with defined inputs and outputs. This compartmentalization is naturally encouraged by java, C#, ruby, and python, but Javascript actually seems to discourage it. The ready availability of anonymous callbacks in JS tends to allow for "function creep", where you blink twice and suddenly you have 50 tangled lines of code without obvious control flow or separation of concerns; sticking to named functions seems like it could definitely help.