What Pinky and the Brain can teach us about Promises and Currys

Node.js is a single-threaded event based platform for working with input and output (I/O).  To avoid blocking the main thread, or event-loop, I/O operations are handled asynchronously (concurrently). Node.js relies on the callback function pattern to deal with asynchronous I/O.  Most methods of the Node.js API accept a callback function as an argument, which allows you to interact with the data from the I/O call inside of the scope of the callback function.  The callback pattern can be challenging to master because callback functions do not return anything.  Therefore, their results cannot be assigned to other variables or passed to other functions like typical functions.

What does this have to do with Pinky and the Brain?  I like to think that the callback pattern is an idea that the Brain would have come up with.  Just like the Brain’s many schemes to take over the world, the callback pattern is complicated and convoluted.   Luckily, we can take full advantage of ES6 Promises with Node.js.  Promises are more simple to use.

Let’s say that Pinky and the Brain are going to take over the world by writing the world’s most awesome command line file reader.  Ok, so that probably wouldn’t work, but it isn’t any less ridiculous than the time that they tried to take over the world by re-making the tear jerker Brian’s Song in the hopes that world leaders would be so devastated by the sad ending that they would simply hand over control to the Brain (which almost worked, by the way).

Pinky and the Brain decide to tackle this problem independently by producing their own modules to read the contents of a file to the command line.  They set up their project directory like this:

/index.js
/package.json
/readme.md
/test.txt
/brain/brain.js
/brain/index.js
/pinky/pinky.js
/pinky/index.js

Both of their modules will be exported to the index.js file in the directory for their module, and then will be exported to the main index.js file at the root of the project folder.

The Brain tackles the problem using good old fashioned imperative syntax and the callback pattern to manage the asynchronous I/O bits.  He starts with a module called brain.js, which will read the contents of a given file and return a callback containing data as a string.

var fs = require('fs')

var brain = function (file, callback) {
  fs.readFile(file, 'utf8', function (e, d) {
    if (e) {
      callback(e)
    }
    return callback(null, d.toString())
  })
}

module.exports = brain

This is pretty straightforward.  He is using the filesystem module (fs), and then, to export the result of the fs.readFile() method and he is wrapping this method in a function called brain that accepts a filename and a callback function as arguments.  If there is an error, his callback is returned with the error.  The Brain’s function returns a function called callback, with null as the argument for error, and the contents of the file as the second argument.  Finally, he assigns this function to the module.exports object.

But, the Brain isn’t done yet.  He still needs to do something with the exported result.  Here is his index.js file.

var brain = require('./brain.js')

var file = process.argv[2]

brain(file, function (err, data) {
  if (err) {
    console.log(err)
  }
  console.log('Brain says:')
  console.log(data)
})

module.exports = brain

To import the module brain.js, the Brain is requiring the module and assigning it to the variable ‘brain’. This allows the functions exported by the module to be invoked.  The Brain is getting the file name from the third argument passed to the command line and assigning it the the variable ”file’, assuming that the first argument will be ‘node’ and the second argument will be a full stop (.) or ‘index.js’.

Next, the Brain is invoking his function brain() from before, and passing the variable file and an anonymous function as arguments.  If there is an error, the error is logged to the console.  The data data from the file is logged to the console.  Finally, the function brain() is assigned to module.exports so that it can be called by another index file.

Let’s say that our test file in the root of our project directory contains ‘Hello world!.  If we want to test the Brain’s module we can do that now.

node ./brain/index.js './test.txt'

The result will look like this.

Brain says:
Hello world!

The Brain’s solution works just fine.  But, it has some problems.  If we want to act upon the data of our file we will need to wrap our actions within the anonymous function passed to brain, or we will need to wrap the function brain in another function which also accepts a callback, and then return that callback with the file data as an argument.  This isn’t a big deal if we only need to act on our data once, but it really sucks if you need to perform a number of actions on that data.  It is also inefficient because we need to invoke an anonymous function each time we act call our data with a callback.

Luckily, Pinky, in his infinite simplicity, devised an alternate strategy using an ES6 promise.  Let’s see what he did.

'use strict'
let fs = require('fs')

let toString = (x) => { return x.toString() }

let narf = (file) => {
  let p = new Promise((resolve, reject) => {
    fs.readFile(file, 'utf8', (e, d) => {
      let result = e ? reject(e) : resolve(d)
      return result
    })
  })
  p = p.then(toString)
  return p
}

module.exports = narf

At a glance, Pinky’s solution almost seems more complicated, but if we look closely, we can see the simplicity of Pinky’s solution paying off very early with some dramatically simplified syntax.  The first thing to note is that Pinky is using strict mode with ‘use strict’.  This allows Pinky to use ‘let’ instead of ‘var’.  If you are creating production code that will be consumed in the browser, it probably isn’t a good idea to use let because not all browsers support its use (even though they should).

However, we aren’t developing for the browser.  Let is declarative.  Look at Pinky’s toString abstraction.  It says let toString equal a function which accepts ‘x’ and returns ‘x’ to string.  Had he used ‘var’ and traditional function syntax, this abstraction would be much more difficult for humans to read and comprehend.

Pinky declares a function called narf().  Narf is a function that accepts a filename as an argument.  Unlike the Brain’s function, Pinky’s narf doesn’t accept a callback, nor does it return one.  Instead, it returns a promise object called ‘p’.  The promise is constructed with an anonymous function that accepts a resolve() function, and a reject() function as arguments.

Pinky uses the fs.readFile() method to read the contents of the file.  Instead of passing the results to a callback function, Pinky uses the ternary operator to assign the reject() function as as the value of the variable result if there is an error reading the file, or resolve() as the value of result if there are no errors.  Result, which may contain either reject() or resolve(), is returned.

You might be thinking, “how is the promise any different that a regular callback?”  After all, the both appear to use the callback pattern.  In fact, you can act upon a promise within the scope of an anonymous function passed as an argument to the promise’s then() method, similar to how the Brain accessed his data using an anonymous function passed as an argument to his brain() function.  But, you definitely don’t have to do this, and that is the beauty of Pinky’s solution.

Take a look at the first line after the body of Pinky’s promise.  It says p = p.then(toString). This could be expressed using an anonymous function.  If it is, we are stuck passing the value with additional callbacks or promises.  Instead, we use p.then() to change our data to  a string and then assign that value to p.  This is the key difference between callbacks and promises.  A callback requires you to pass values acquired with asynchronous methods with additional callbacks, while promises allow you to interact with the value of the asynchronous data using the public methods of the promise object.  We can then assign the mutated value to a variable, and carry on reasoning with our programs in sequential order.

Here is Pinky’s index.js file, where he logs the file contents to the console.

'use strict'
let narf = require('./pinky.js')
let file = process.argv[2]

file = narf(file)

console.log('Pinkey says:')
file.then(console.log)

This is significantly less verbose than the Brain’s index.js file.  But, Pinky doesn’t care for the promise.then() method.  Pinky likes to think of things in terms of narfs and poits.  To Pinky, a narf is the value of a promise, and a poit is something that acts on the value of a promise.

Pinky point of view of a narf and poit.

Pinky wants to poit his narfs, so he comes up with a way to curry the result of p.then() so that it can be composed with other functions.

let poit = (f) => {
  return (p) => {
    let r = p.then(f)
    return r
  }
}

The function point() is a function that returns another function, which returns the result of p.then for a given promise p.  This function can be composed in a couple of different ways.

let toUpper = (x) => { return x.toUpperCase() }
let upper = poit(toUpper)
file = upper(file)
poit(console.log)(file) // prints file data to console

Thanks to poit, Pinky gains the ability to pre-compose functions that can act on the value of a promise.  In addition, poit can accept a function and a promise as arguments and apply the function to the value of the promise.  If Pinky wants to carry the results of one of the poits forward, he just needs to assign it to a variable, for example file = upper(file).

Here is Pinky’s index.js file altogether.

'use strict'

let narf = require('./pinky.js')
let file = process.argv[2]

let poit = (f) => {
  return (p) => {
    let r = p.then(f)
    return r
  }
}

let toUpper = (x) => { return x.toUpperCase() }

file = narf(file)
let upper = poit(toUpper)
file = upper(file)
console.log('Pinky says:')
poit(console.log)(file)

module.exports = poit

Here is the result if we execute Pinky’s code on our test file from the root of the project directory.

node ./pinky/index.js './test.txt'
Pinky says:
HELLO WORLD!

Finally, pinky and the Brain combine their modules into an index.js file at the root of their project.

var pinkey = require('./pinky/index.js')
var brain = require('./brain/index.js')

Since their modules were invoked in their respective index.js files (not recommended), we don’t need to invoke their modules again in the root index.js file.

Now the code can be executed from the root of the project directory to see both of their modules in action.

node . 'test.txt'
Pinky says:
HELLO WORLD!

Brain says:
Hello world!

To see the whole project put together, check it out on github.