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.