144 lines
3.9 KiB
Markdown
144 lines
3.9 KiB
Markdown
|
# Controlling Flow: callbacks are easy
|
||
|
|
||
|
## What's actually hard?
|
||
|
|
||
|
- Doing a bunch of things in a specific order.
|
||
|
- Knowing when stuff is done.
|
||
|
- Handling failures.
|
||
|
- Breaking up functionality into parts (avoid nested inline callbacks)
|
||
|
|
||
|
|
||
|
## Common Mistakes
|
||
|
|
||
|
- Abandoning convention and consistency.
|
||
|
- Putting all callbacks inline.
|
||
|
- Using libraries without grokking them.
|
||
|
- Trying to make async code look sync.
|
||
|
|
||
|
## Define Conventions
|
||
|
|
||
|
- Two kinds of functions: *actors* take action, *callbacks* get results.
|
||
|
- Essentially the continuation pattern. Resulting code *looks* similar
|
||
|
to fibers, but is *much* simpler to implement.
|
||
|
- Node works this way in the lowlevel APIs already, and it's very flexible.
|
||
|
|
||
|
## Callbacks
|
||
|
|
||
|
- Simple responders
|
||
|
- Must always be prepared to handle errors, that's why it's the first argument.
|
||
|
- Often inline anonymous, but not always.
|
||
|
- Can trap and call other callbacks with modified data, or pass errors upwards.
|
||
|
|
||
|
## Actors
|
||
|
|
||
|
- Last argument is a callback.
|
||
|
- If any error occurs, and can't be handled, pass it to the callback and return.
|
||
|
- Must not throw. Return value ignored.
|
||
|
- return x ==> return cb(null, x)
|
||
|
- throw er ==> return cb(er)
|
||
|
|
||
|
```javascript
|
||
|
// return true if a path is either
|
||
|
// a symlink or a directory.
|
||
|
function isLinkOrDir (path, cb) {
|
||
|
fs.lstat(path, function (er, s) {
|
||
|
if (er) return cb(er)
|
||
|
return cb(null, s.isDirectory() || s.isSymbolicLink())
|
||
|
})
|
||
|
}
|
||
|
```
|
||
|
|
||
|
# asyncMap
|
||
|
|
||
|
## Usecases
|
||
|
|
||
|
- I have a list of 10 files, and need to read all of them, and then continue when they're all done.
|
||
|
- I have a dozen URLs, and need to fetch them all, and then continue when they're all done.
|
||
|
- I have 4 connected users, and need to send a message to all of them, and then continue when that's done.
|
||
|
- I have a list of n things, and I need to dosomething with all of them, in parallel, and get the results once they're all complete.
|
||
|
|
||
|
|
||
|
## Solution
|
||
|
|
||
|
```javascript
|
||
|
var asyncMap = require("slide").asyncMap
|
||
|
function writeFiles (files, what, cb) {
|
||
|
asyncMap(files, function (f, cb) {
|
||
|
fs.writeFile(f, what, cb)
|
||
|
}, cb)
|
||
|
}
|
||
|
writeFiles([my, file, list], "foo", cb)
|
||
|
```
|
||
|
|
||
|
# chain
|
||
|
|
||
|
## Usecases
|
||
|
|
||
|
- I have to do a bunch of things, in order. Get db credentials out of a file,
|
||
|
read the data from the db, write that data to another file.
|
||
|
- If anything fails, do not continue.
|
||
|
- I still have to provide an array of functions, which is a lot of boilerplate,
|
||
|
and a pita if your functions take args like
|
||
|
|
||
|
```javascript
|
||
|
function (cb) {
|
||
|
blah(a, b, c, cb)
|
||
|
}
|
||
|
```
|
||
|
|
||
|
- Results are discarded, which is a bit lame.
|
||
|
- No way to branch.
|
||
|
|
||
|
## Solution
|
||
|
|
||
|
- reduces boilerplate by converting an array of [fn, args] to an actor
|
||
|
that takes no arguments (except cb)
|
||
|
- A bit like Function#bind, but tailored for our use-case.
|
||
|
- bindActor(obj, "method", a, b, c)
|
||
|
- bindActor(fn, a, b, c)
|
||
|
- bindActor(obj, fn, a, b, c)
|
||
|
- branching, skipping over falsey arguments
|
||
|
|
||
|
```javascript
|
||
|
chain([
|
||
|
doThing && [thing, a, b, c]
|
||
|
, isFoo && [doFoo, "foo"]
|
||
|
, subChain && [chain, [one, two]]
|
||
|
], cb)
|
||
|
```
|
||
|
|
||
|
- tracking results: results are stored in an optional array passed as argument,
|
||
|
last result is always in results[results.length - 1].
|
||
|
- treat chain.first and chain.last as placeholders for the first/last
|
||
|
result up until that point.
|
||
|
|
||
|
|
||
|
## Non-trivial example
|
||
|
|
||
|
- Read number files in a directory
|
||
|
- Add the results together
|
||
|
- Ping a web service with the result
|
||
|
- Write the response to a file
|
||
|
- Delete the number files
|
||
|
|
||
|
```javascript
|
||
|
var chain = require("slide").chain
|
||
|
function myProgram (cb) {
|
||
|
var res = [], last = chain.last, first = chain.first
|
||
|
chain([
|
||
|
[fs, "readdir", "the-directory"]
|
||
|
, [readFiles, "the-directory", last]
|
||
|
, [sum, last]
|
||
|
, [ping, "POST", "example.com", 80, "/foo", last]
|
||
|
, [fs, "writeFile", "result.txt", last]
|
||
|
, [rmFiles, "./the-directory", first]
|
||
|
], res, cb)
|
||
|
}
|
||
|
```
|
||
|
|
||
|
# Conclusion: Convention Profits
|
||
|
|
||
|
- Consistent API from top to bottom.
|
||
|
- Sneak in at any point to inject functionality. Testable, reusable, ...
|
||
|
- When ruby and python users whine, you can smile condescendingly.
|