Separate work from control flow using functional programming

Thunkifying is currying, argument reordering and middleware stacks.

Thunkify = 1 step curry

Take a typical Node function, like fs.readFile. It takes as arguments a file name, an optional encoding and a callback to be executed once the file has been loaded from disk

:::javascript
fs.readFile('./foo.txt', 'utf8', function cb(err, text) {
    console.log(text);
});

When we call fs.readFile we need to make two decisions at once: the load parameters (filename and encoding), and prepare the callback function to be executed. Sometimes it makes a lot of sense to decouple these 2 decisions. First, prepare the arguments, then prepare the callback. Finally, execute the function.

To transform a callback-returning function into 2-step function has its own name in Node community: to thunkify a function.

:::javascript
var thunkify = require('thunkify');
var read = thunkify(fs.readFile);
read('package.json', 'utf8')(function(err, str) { ... });

thunkify is a higher-order function that wraps its input (a normal callback-returning function) into a new function. First execution of read returns another function.

:::javascript
var read = thunkify(fs.readFile);
typeof read // 'function'
var preparedRead = read('package.json', 'utf8');
typeof preparedRead; // 'function'
preparedRead(cb);

We separated actual work (read function) from the callback execution (preparedRead function).

Thunkify is a name for the feature generally known by its other name: curry. Currying a function returns a new function that can be used the same way as a function returned by thunkify.

:::javascript
var R = require('ramda');
var read = R.curry(fs.readFile);
typeof read // 'function'
var preparedRead = read('package.json', 'utf8');
typeof preparedRead // 'function'

In fact, the read function returned by the R.curry(fs.readFile) is more flexible than the function returned from thunkify, because it can separate each argument into separate function call.

:::javascript
var read = R.curry(fs.readFile);
read('package.json')('utf8')(cb); // any number of arguments can be applied at a time
var readThunk = thunkify(fs.readFile);
readThunk('package.json', 'utf8')(cb); // all arguments up to callback have to be applied together

Other control flow tricks

Once we are comfortable transforming fs.readFile function into curried one, we can apply other tricks to fit it to our needs. For example, if we want to build the control flow structure first, and then read the file, we could change the order of arguments in fs.readFile.

:::javascript
// flip first and last arguments to the function
function flip(fn) {
  return function flipped() {
    var args = Array.prototype.slice.call(arguments, 0);
    var cb = args.shift();
    args.push(cb);
    return fn.apply(null, args);
  };
}
var fs = require('fs');
var read = flip(fs.readFile);
function cb(err, text) {
  console.log(text);
}
read(cb, './package.json', 'utf8');
// prints package.json

Now we can partially apply the callback before knowing the name of the file to read.

:::javascript
var printFile = read.bind(null, cb);
printFile('./package.json', 'utf8');
// prints package.json

We have prepared the control flow (read file, then print) using partial application read.bind. Then we executed the functions by calling the prepared function with file name and encoding. This pattern can be used to prepare customs stacks of middleware.

Middleware stacks

Take a typical connect / express middleware function. It takes in request and response objects and a function to call to continue down the stack (if needed).

:::javascript
app.use(function printRequestTimestamp(req, res, next) {
  console.log('Time:', Date.now());
  next();
});
app.use(function printRequestMethod(req, res, next) {
  console.log('Request Type:', req.method);
  next();
});

Every request will first print the timestamp, then method ('GET', 'POST', etc). Can we combine the two separate functions printRequestTimestamp and printRequestMethod into single one? This would be very useful for unit testing and for executing certain functions for a specific url.

:::javascript
function printRequestTimestamp(req, res, next) {
  console.log('Time:', Date.now());
  next(req, res);
}
function printRequestMethod(req, res, next) {
  console.log('Request Type:', req.method);
  next(req, res);
}
function last() {} // to server as next inside printRequestMethod
var printTimestamp = flip(printRequestTimestamp);
var printMethod = flip(printRequestMethod);

Let us assemble single function to print timestamp, then method.

:::javascript
var printBoth = printTimestamp.bind(null, printMethod.bind(null, last));

The printBoth function just sits there, waiting for request and response (arguments).

:::javascript
printBoth(req, res);
// prints timestamp
// prints method

I used this approach when creating a series of actions to perform for matching request urls in testing proxy turtle-run. For matching url the proxy can slow down / stop request. This means the request processing stack has to be constructed dynamically from the config file.

:::javascript
function printRequestTimestamp(req, res, next) {
  console.log('Time:', (new Date()).toTimeString());
  next(req, res);
}
function printRequestMethod(req, res, next) {
  console.log('Request Type:', req.method);
  next(req, res);
}
var allRequestMethods = [printRequestTimestamp, printRequestMethod];

Let us construct a single function that will execute the middleware stack.

:::javascript
var actionStack = allRequestMethods.reduce(function (prevFn, fn) {
  return flip(fn).bind(null, prevFn);
}, function end() {});
actionStack({ method: 'GET' }, null);
// output
Request Type: GET
Time: 22:34:36 GMT-0500 (EST)

We are taking each function in turn, flip the callback argument to be the first and then bind the function assembled from the previous functions. The last function called on the stack is an empty function end.

We can even avoid calling next(req, res); and just use next() by constructing the stack and binding the arguments using lexical scope

:::javascript
function printRequestTimestamp(req, res, next) {
  console.log('Time:', (new Date()).toTimeString());
  next();
}
function printRequestMethod(req, res, next) {
  console.log('Request Type:', req.method);
  next();
}
function process(req, res) {
    var actionStack = allRequestMethods.reduce(function (prevFn, fn) {
      return flip(fn).bind(null, prevFn, req, res);
    }, function end() {});
    actionStack();
}
process({ method: 'GET' }, null);

Conclusion

Reordering arguments and separating out the control flow logic leads to flexible code in a few lines of code.