slide flow control

A flow control library that fits in a slideshow

135
24
JavaScript

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)
// 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

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
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
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
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.