3 ms·
Allow me to make both a theoretical and practical argument. It's been said that "callbacks are imperative; promises are functional". It's true. Furthermore, ca
by brandonbloom 10y ago
Allow me to make both a theoretical and practical argument.
It's been said that "callbacks are imperative; promises are functional". It's true. Furthermore, callbacks structure control flow and promises structure data flow. Instruction scheduling is an explicit sequence with callbacks (well, assuming the API calls you back precisely once), but scheduling is an implicit topological sort of the directed acyclic dependency graph of promises.
Sometimes, when it comes to side effects, implicit scheduling is less than ideal. You need specific things to happen in a specific order. The solution is to introduce data dependencies to force a particular schedule. This is precisely what is done by monads in Haskell. However, unlike Haskell, JavaScript doesn't have "do" syntax, so there's no convenient notation for a nested chain of bindings.
The result of not having monadic binding syntax is that you wind up with some funky nested chain of getY(x).then(y => y.then(z => z.then(.... OR you have to declare variables up top, flatten your then blocks in to a promise chain, and often ignore intermediate values:
let y, z;
getY(x)
.then(returnedY => { y = returnedY; return zPromise(); )
.then(returnedZ => { z = returnedZ; return ......; }
.then(......another side effect.....)
.then(_ => f(y, z))
async/await neatly eliminates this problem by reusing the traditional coupling of data flow dependencies with first-order control flow dependencies.
Practically:
let y = await getY(x);
let z = await getZ(y);
......another side effect....
return f(y, z)
- andrewstuart2 10y ago> It's been said that "callbacks are imperative; promises are functional". It's true. Furthermore, callbacks structure control flow and promises structure data flow. Do you have any good escapes l examples of further reading on this? I'm not sure I agree with this statement, or perhaps don't fully understand it. It seems like both cases boil down to functions as data (function pointers essentially) that are called at the end of some event or chain of events. To me, they seem to be the exact same thing but worth promises being much more human readable.
- chenglou 10y agoTo give a brief "explanation", look at the type signature of a callback-based API: readThisFile: string -> (string -> void) -> void Usage: readThisFile('foo.txt', result => ...) You have a double void here. Which is the hand-wavy justification as to why callbacks don't compose. On the other hand, a promise's value is Promise<actualTypeOfValue>, which you can pass around as data without asking the other end what it wants to do with the value (aka decoupling). You do have the imperative action at the top to kick the whole chain into motion, but the intermediate stuff are most of the time side-effect-free. Hopefully that was clear enough to see that passing actions around is akin to controlling the flow with e.g. if-else, and that passing data around is declaring how your tubes/rail tracks/whatever analogy for monads are constructed, aka data flow.
- brandonbloom 10y agoYou can google that quote and find more, but the control flow vs data flow formulation is not widely discussed from what I've seen.
- tel 10y agoIt's widely discussed in Haskell communities, fwiw ;)
- Bahamut 10y agoThis is not quite true with regards to promises - one can do getY(x) .then(returnedY => Promise.all([Promise.resolve(y), zPromise()]) .then([y, z] => { ...another side effect...; return Promise.all([Promise.resolve(y), Promise.resolve(z), sideEffectPromise()]) .then([y, z, _] => f(y, z)); Not the cleanest, but you preserve the isolation without relying on polluted variables bleeding into function scope. In addition, one can do proper error handling via .catch as opposed to the brute force unnecessary catchall that is a try-catch.
- brandonbloom 10y ago1) Yes, you can restructure/destructure variables at each step, but you're just unifying the control flow and data flow by hand. Yet another flavor of human compiler. 2) catch's entire purpose is to be a catch-all for unexpected errors. For expected errors, it's better to have explicit error values. Bluebird.js at least lets you differentiate .catch from .error, where .error handles the explicit error values generated from traditional Node.js callbacks.
- z3t4 10y ago99.9% of all JavaScript is handling state and events in a user interface (web page). Ex: You have two buttons, and when both of them has been pressed, a image of a cat is displayed. >> functional event-driven code with contextual closures This paradigm works very well on the server side too. Ex: A HTTP requests makes a query to a database server, then returns the result to the browser This gives parallelism without the complexity of threads.