Promises
and
Generators

Control Flow Utopia

node --harmony-generators script.js

Slides: pag.forbesl.co.uk

Sequence 1

function count(n){
  var res = []
  for (var x = 0; x < n; x++) {
    res.push(x)
  }
  return res
}
for (var x of count(5)) {
  console.log(x)
}

Sequence 2

function count(){
  var res = []
  for (var x = 0; true; x++) {
    res.push(x)
  }
  return res
}
for (var x of count()) {
  console.log(x)
}

Sequence 3

function* count(){
  for (var x = 0; true; x++) {
    yield x
  }
}

for (var x of count()) {
  console.log(x)
}

Take

function* take(list, n){
  var i = 0
  for (var x of list) {
    if (n === i++) {
      return
    }
    yield x
  }
}

for (var x of take(count(), 5)) {
  console.log(x)
}

Sync JSON

function readJSONSync(filename){
  return JSON.parse(fs.readFileSync(filename, 'utf8'))
}

Async JSON 1

function readJSON(filename, callback){
  fs.readFile(filename, 'utf8', function (err, res){
    if (err) return callback(err)
    callback(null, JSON.parse(res))
  })
}
  • Conflates the input with the output
  • Doesn't work with control flow primitives
  • Doesn't handle errors

Async JSON 2

function readJSON(filename, callback){
  fs.readFile(filename, 'utf8', function (err, res){
    if (err) return callback(err)
    try {
      callback(null, JSON.parse(res))
    } catch (ex) {
      callback(ex)
    }
  })
}
  • Conflates the input with the output
  • Doesn't work with control flow primitives
  • Handles errors in JSON.parse
  • Double handles errors in the callback
var n = 0
readJSON(filename, function (err, res){
  throw new Error('Unerlated error ' + (n++))
})

Async JSON 3

function readJSON(filename, callback){
  fs.readFile(filename, 'utf8', function (err, res){
    if (err) return callback(err)
    try {
      res = JSON.parse(res)
    } catch (ex) {
      return callback(ex)
    }
    callback(null, res)
  })
}
  • Conflates the input with the output
  • Doesn't work with control flow primitives
  • But it does handle errors

Sync JSON

function readJSONSync(filename){
  return JSON.parse(fs.readFileSync(filename, 'utf8'))
}

Imagine

  • Asynchronous methods still return values
  • The values cannot be used directly
  • The values can be awaited

Promise

  • A promise represents the result of an asynchronous operation
  • A promise can be pending, fulfilled or rejected
function readFile(filename, enc){
  return new Promise(function (fulfill, reject){
    fs.readFile(filename, enc, function (err, res){
      if (err) reject(err)
      else fullfill(res)
    })
  })
}

Promise JSON A

function readJSON(filename){
  return new Promise(function (fulfill, reject){
    readFile(filename, 'utf8').done(function (res){
      try {
        fullfill(JSON.parse(res))
      } catch (ex) {
        reject(ex)
      }
    }, reject)
  })
}
  • Doesn't conflate input with output
  • Doesn't work with control flow primitives
  • Requires extra work to handle errors

.then is to .done

as

.map is to .forEach

Promise JSON B

function readJSON(filename){
  return readFile(filename, 'utf8').then(function (res){
    return JSON.parse(res)
  })
}
function readJSON(filename){
  return readFile(filename, 'utf8').then(JSON.parse)
}
  • Doesn't conflate input with output
  • Doesn't work with control flow primitives
  • Handles errors

Generator JSON

What if yield could be used to "await" a promise?

var readJSONSync = function (filename){
  return JSON.parse(fs.readFileSync(filename, 'utf8'))
}
var readJSON = async(function *(filename){
  return JSON.parse(yield readFile(filename, 'utf8'))
})
  • Doesn't conflate input with output
  • Works with control flow primitives
  • Handles errors
  • Looks asynchronous

A sequence of operations

var get = async(function *(){
  var left = yield readJSON('left.json')
  var right = yield readJSON('right.json')
  return {left: left, right: right}
})

Parallel operations

var get = async(function *(){
  var left = readJSON('left.json')
  var right = readJSON('right.json')
  return {left: yield left, right: yield right}
})

Try/Catch

var tryGet = async(function *(key, defaultValue){
  var result
  try {
    result = yield get(key)
  } catch (ex) {
    result = defaultValue
  }
  return result
})

For

var uploadDocuments = async(function *(documents){
  for (var document of documents) {
    yield upload(document)
  }
})
var uploadDocumentsParallel = async(function *(documents){
  var operations = []
  for (var document of documents) {
    operations.push(upload(document))
  }
  for (var operation of operations) {
    yield operation
  }
})

How it works - Fulfilling

  • yield foo is an expression
  • A generator can be manually operated via .next(value)
function* demo() {
  var res = yield 10
  assert(res === 32)
  return 42
}
var d = demo()
var resA = d.next()
// => {value: 10, done: false}
var resB = d.next(32)
// => {value: 42, done: true}
// d.next() - THROWS!!!

How it works - rejecting

  • A generator can be sent an exception via .throw(error)
var sentinel = new Error('foo')
function* demo() {
  try {
    yield 10
  } catch (ex) {
    assert(ex === sentinel)
  }
}
var d = demo()
d.next()
// => {value: 10, done: false}
d.throw(sentinel)

Putting it all together

function async(makeGenerator){
  return function (){
    var generator = makeGenerator.apply(this, arguments)
    
    function handle(result){ // { done: [Boolean], value: [Object] }
      if (result.done) return result.value
      
      return result.value.then(function (res){
        return handle(generator.next(res))
      }, function (err){
        return handle(generator.throw(err))
      })
    }
    
    return handle(generator.next())
  }
}

Why Promises?

Streamline

function readJSON(filename, _){
  return JSON.parse(fs.readFile(filename, 'utf8', _))
}
  • Works with control flow primitives
  • Handles errors
  • Conflates input with output
  • Looks entirely synchronous
  • Compiles to pretty ugly code

Suspend

suspend(function *(resume){
  return JSON.parse(yield fs.readFile(filename, 'utf8', resume))
})
  • Works with control flow primitives
  • Handles errors
  • Conflates input with output
  • Isn't doing what it looks like
  • Can't do parallel operation

CO

function readFile(filename, encoding){
  return function (callback){
    return fs.readFile(filename, encoding, callback)
  }
}
co(function *(){
  return JSON.parse(yield readFile(filename, 'utf8'))
})
  • Works with control flow primitives
  • Handles errors
  • Can't do parallel operation
  • Can't share and cache async operations

Conclusion A

Promises let us turn

function readJSON(filename, callback){
  fs.readFile(filename, 'utf8', function (err, res){
    if (err) return callback(err)
    try {
      res = JSON.parse(res)
    } catch (ex) {
      return callback(ex)
    }
    callback(null, res)
  })
}

INTO

function readJSON(filename){
  return readFile(filename, 'utf8').then(JSON.parse)
}

Conclusion B

Generators let us turn

function get(filename){
  return readJSON('left.json').then(function (left){
    return readJSON('right.json').then(function (right){
      return {left: left, right: right}
    })
  })
}

INTO

var get = async(function *(){
  var left = yield readJSON('left.json')
  var right = yield readJSON('right.json')
  return {left: left, right: right}
})

Forbes Lindesay

Social Networks

  • Twitter: @ForbesLindesay
  • GitHub: @ForbesLindesay
  • Blog: www.forbeslindesay.co.uk

Open Source

  • Jade (maintainer)
  • Browserify Middleware (creator)
  • esdiscuss.org (creator)
  • regexplained.co.uk (creator)

Work

  • Red Gate

Slides

  • pag.forbesl.co.uk