Node.js Flow – Callback Hell vs. Async vs. Highland Streams
blog.vullum.io
blog.vullum.io
.flatMap(_.wrapCallback(one))
.flatMap(_.wrapCallback(two))
...
or .then(Promise.promisifyAll(one))
.then(Promise.promisifyAll(two))
...
are noise making the signal difficult to read. It should come down to nothing more than one | two
plus the error handling clause. That's not JavaScript but you got the idea. Syntax is important. I still have bad dreams about arrays() hell in an app written by a customer with CakePHP 1 (a Rails clone with an approach that didn't suit PHP well). I'll skip over the $this-> mess.I'm not much into asynchronous languages. Is there any examples around with a built in and slim syntax for concatenating callbacks? Thanks.
do one
two
because it's pretty well understood there that syntax can be a big differentiator.What's more is that this syntax adopts for the binding aspect of flatMap. So if two is dependent on one...
do output <- one
two output
which is a shortcut for one >>= two
which might be Promise.promisifyAll(one)
.then(function (output) {
return Promise.promisifyAll(two(output))
})
in JavascriptBut in this specific case of just tying two bit of execution together, I'd observe that Erlang is
One(),
Two()
and Go would be One()
Two()
and that's all regardless of whether One and Two have 0 accesses to IO or do IO dozens of times, and One and Two may also freely call other functions that do arbitrary IO with no further ceremony. The "solution" Erlang and Go offer to callback hell is simply not to have it, and pretty much no matter how you slice it, not having the problem in the first place beats any solution the JS community may or may not settle on.So it kind of comes down to whether you want to shave off ceremony for concurrency or for all monadic/sequencing operations at once. Honestly, that's a tradeoff.
Yeah it's called any language that offers good abstractions for concurrency. Elixir/Erlang, Haskell, Clojure and Go are a few good examples.
function compose(f, g) {
return function inner(x) {
return f(g(x));
}
}
this is not the right kind of composition for something like nested callbacks. For that you need slightly more.Edit:
It's actually the "apply" operator which looks like
function apply(f) {
return function applied(x) {
return f(x);
}
}
Apply and compose work together nicely. For instance, in Haskell apply is ($) and compose is (.), so you can build a compositional pipeline and apply it like f . g . h $ x
or like f $ g $ h $ x
since association goes this way f $ (g $ (h $ x))Deleted comment
In a lot of cases compose and |> will be similar due to operations that are commutative, but they are semantically different.
one.pipe(two)[0]: http://6to5.org/
[1]: https://iojs.org/ [2]: http://koajs.com/ [3]: https://github.com/tj/co
How does the client or the dev benefit from the non-blocking approach here? At the end of the day he is describing a sequence of actions the depend on each other. Why not block then?
I really dont want to sound arrogant, but I am just curious what the benefits are that I dont know of.
Nowadays, there's also the fact that if you want to use Javascript on the backend, Node.js is the de facto standard. You don't necessarily have to do async I/O but you'd be fighting against the whole ecosystem by not doing so.
[0] http://en.wikipedia.org/wiki/Preemption_(computing)#Preempti...
.then(Promise.promisifyAll(process1))
.then(Promise.promisifyAll(process2))
.then(Promise.promisifyAll(process3))
Shouldn't it be just Promise.promisify[1]. Not that it matters, promisifyAll does the same thing, but also looks for child functions to promisify.[1] https://github.com/petkaantonov/bluebird/blob/master/API.md#...
Personally I think the highland approach is too verbose, while the bluebird promises is the one I prefer for both readability and functionality.
Does anyone has used them in big production systems? Are you happy with them?
It's fast to boot and is great for error handling. I highly recommend it.
I've used them a fair bit in production, and I'd say yes, I'm quite happy with them.
Edit: I've used async too, but I feel that async does not scale well in terms of complexity. When you start needing solid error handling while dealing with multiple async processes with complex mixtures of parallel and sequential...bluebird works, async just ends up as a mess. In my experience, anyhow. Either lib works great for simple uses cases though. :)
If willing to take a performance hit (they will remain faster than caolan's async or native promises though), they're also usable in production.
This is the approach that Erlang (and therefore Elixir) takes.
Can you expand more on your comment and/or point to some references?
The overarching solution is to be "flat" by using small, composable functions. Lazy.js and Highland.js force the user to do this but require learning a specific syntax. Streams make this technique easier to reason about (easier say, than Transducers or Generators), but the goal is the same: operate on one object at a time with one purpose, and return a result in a standard manner so it's easy to consume with the next function. This is good practice, but leaning on one of these libraries is an excuse to force the practice.
Chained callbacks is JS looking from the inside out; looking from the outside in, we see this practice is solved by many other languages [1] -- not just serial calls but parallel also! C#, Scala, Python, Java, Dart, Spiderjs and more have implementations of Futures [2].
Fibers implement Futures but require a native module for V8 -- since they are not portable outside Node they don't have great adoption and aren't used in almost any node.js modules: only ~100 out of the endless ecosystem [3].
Fibers, Async.js, and Highland also aren't syntactically an improvement over Promises -- the point of programming, which devs will get tunnel vision and forget -- is to leverage the language to do more work for less keystrokes/manhours [4]. The compiler should create control flow for my code, not the other way around. There should be no reason to learn a complex API just to get serial/parallel calls, when a simple keyword addition to the language will suffice.
Not to mention, await/defer is so readable that new devs can start working with async code right away, without needing to research. This is not the case with promises/fibers/asyncjs/highland or whatever flavor-of-the-month a shop has picked for control flow.
[1] https://github.com/KjellSchubert/promise-future-task
[2] http://en.wikipedia.org/wiki/Futures_and_promises#List_of_im...
[3] https://www.npmjs.com/package/fibers
[4] http://blog.teamleadnet.com/2013/08/async-await-in-net-is-no...
Webbies seriously have Stockholm syndrome with this language.