Skip to content

Instantly share code, notes, and snippets.

@cqfd
Last active August 29, 2015 14:23
Show Gist options
  • Save cqfd/9b9e37007f06e8a23c2a to your computer and use it in GitHub Desktop.
Save cqfd/9b9e37007f06e8a23c2a to your computer and use it in GitHub Desktop.
From ES6 generators to Go-style CSP.

Generators

function* teammate(name) {
  console.log(`Hi! I'm ${name} and I'm your teammate.`)
  const num = yield "I need a number!"
  const num2 = yield "I need another number!"
  console.log("Ok, here's my work.")
  return num * num2
}

const alice = teammate('Alice')

Alice hasn't logged anything yet; calling a function* instantiates a generator, but it doesn't run any of the generator's code. You tell a generator to run by calling its next method:

var { done, value } = alice.next()
// Hi! I'm Alice and I'm your teammate.
// done === false
// value === 'I need a number!'

Alice starts working and executes her code until she hits her first sticking point; apparently she needs a number before she can continue. We can unstick her by calling next again, passing a number as an argument:

var { done, value } = alice.next(1)
// done === false
// value === 'I need another number!'

var { done, value } = alice.next(2)
// Ok, here's my work.
// done === true
// value === 1 * 2

The values we pass in with next supply Alice with whatever she's waiting for. The very first time we called next we didn't pass in anything at all, because Alice wasn't waiting for anything; she hadn't even started running yet. The value we passed to each subsequent call to next was returned to Alice by one of her yields.

Just as you can delegate work to a subroutine, you can delegate work to another generator with yield*:

function* waitForGreatWork() {
  while (true) {
    const attempt = yield* teammate('Alice')
    if (attempt > 100) return attempt
    else console.log('Not good enough!')
  }
}

const workGen = waitForGreatWork()

var { done, value } = workGen.next()
// Hi! I'm Alice and I'm your teammate.
// done === false
// value === 'I need a number!'

var { done, value } = workGen.next(7)
// done === false
// value === 'I need another number!'

var { done, value } = workGen.next(8)
// Ok, here's my work.
// Not good enough!
// Hi! I'm Alice and I'm your teammate.
// done === false
// value === 'I need a number!'

Values we pass through next thread through Alice after Alice, and each Alice's return value is bound to attempt.

Eventually workGen will finish too, once we pass in neat enough numbers:

var { done, value } = workGen.next(123)
// done === false
// value === 'I need another number!'

var { done, value } = workGen.next(456)
// Ok, here's my work.
// done === true
// value === 123 * 456

When you delegate to a regular function call, you tell the function to crunch away and then give you its return value. Delegating to a generator function call is the same idea, just with a new keyword: you tell the generator to crunch away, through as many yields as necessary, until it can hand you its return value.

Regular types: a, a -> b, etc. Generator types: *a, a -> *b, etc. A *a is an instantiated generator that will eventually return an a. A function* that takes an a and returns a generator that will eventually return a b then has type a -> *b.

Generators and promises

Instead of yielding strings when we're stuck, it would be nice to be able to yield promises:

const sleep = ms => new Promise(awaken => setTimeout(awaken, ms))

function* waitForNewMessages(since) {
  while (true) {
    const msgs = yield $.getJSON('/messages', { since })
    if (msgs.length) return msgs
    yield sleep(5000)
  }
}

We had to manually unstick Alice by calling next, but we can unstick promises automatically:

Simple, happy-path version that doesn't handle promises that reject:

function pogo(star) {
  const gen = star()
  return new Promise(ok => {
    bounce()
    function bounce(input) {
      const output = gen.next(input)
      if (output.done) ok(output.value)
      else output.value.then(bounce)
    }
  })
}

Complete version that handles promises that reject:

function pogo(star) {
  const gen = star()
  return new Promise(ok => {
    bounce()
    function bounce(input) { decode(gen.next(input)) }
    function toss(error) { decode(gen.throw(error)) }
    function decode(output) {
      if (output.done) ok(output.value)
      else output.value.then(bounce, toss)
    }
  })
}

The idea is that whenever the generator yields a promise, pogo waits for it to resolve or reject. If it resolves, the resolution gets bounced into the generator with next, and if it rejects, the rejection gets tossed in with throw. Hopefully the generator eventually returns something, and pogo promises it.

Types: pogo : (*a | () -> *a) -> promise[a].

Generators and CSP

function pogo(star) {
  const gen = star()
  return new Promise(ok => {
    bounce()
    function bounce(input) { decode(gen.next(input)) }
    function toss(err) { decode(gen.throw(err)) }
    function decode(output) {
      if (output.done) return ok(output.value)
      
      const op = output.value
      if (op instanceof Channel) return op.take().then(bounce)
      if (op instanceof Put) return op.channel.put(op.value).then(bounce)
    }
  })
}

class Channel {
  constructor() {
    this.putings = []
    this.takings = []
  }
  put(value) {
    return new Promise(ok => {
      if (!this.takings.length) return this.putings.push({value, ok})
      
      const taking = this.takings.shift()
      taking.ok(value)
      
      ok()
    })
  }
  take() {
    return new Promise(ok => {
      if (!this.putings.length) return this.takings.push({ok})
      
      const puting = this.putings.shift()
      puting.ok()
      
      ok(puting.value)
    })
  }
}
const chan = () => new Channel

class Put {
  constructor(channel, value) {
    this.channel = channel
    this.value = value
  }
}
const put = (ch, val) => new Put(ch, val)
const ch = chan()

pogo(function*() {
  while (true) {
    yield put(ch, 'tick')
    yield sleep(1000)
  }
})
pogo(function*() {
  while (true) {
    yield put(ch, 'tock')
    yield sleep(1000)
  }
})

pogo(function*() {
  while (true) console.log(yield ch)
})

TODO: Adding race/alts/select really does make things more complicated...

function pogo(star) {
  const gen = star()
  return new Promise(ok => {
    bounce()
    function bounce(input) { decode(gen.next(input)) }
    function toss(err) { decode(gen.throw(err)) }
    function decode(output) {
      if (output.done) return ok(output.value)
      
      const op = output.value
      if (isPromise(op)) op.then(bounce, toss)
      if (op instanceof Channel) op.take(gen).then(bounce)
      if (op instanceof Put) op.channel.put(gen, op.value).then(bounce)
      if (op instanceof Race) {
        const race = {finished: false}
        for (let op of op.ops) {
          if (isPromise(op)) {
            op.then(v => {
              if (!race.finished) {
                race.finished = true
                bounce(v)
              } 
            }, e => {
              if (!race.finished) {
                race.finished = true
                toss(e) 
              } 
            })
          }
          if (op instanceof Channel) {
            op.take(race).then(value =>
              bounce({value, channel: op}))
          }
          if (op instanceof Put) {
            op.channel.put(race, op.value).then(value =>
              bounce({value, channel: op.channel}))
          }
        }
      }
    }
  })
}

class Race {
  constructor(ops) {
    this.ops = ops
  }
}
const race = ops => new Race(ops)
const racing = x => x.finished !== undefined

class Channel {
  constructor() {
    this.putings = []
    this.takings = []
  }
  put(puter, value) {
    return new Promise((ok, notOk) => {
      if (puter.finished) return notOk()
      
      this.takings = this.takings.filter(t => !t.taker.finished)
      if (!this.takings.length) return this.putings.push({puter, value, ok})
      
      const taking = this.takings.shift()
      if (racing(taking.taker)) taking.taker.finished = true
      taking.ok(value)
      
      ok()
    })
  }
  take(taker) {
    return new Promise((ok, notOk) => {
      if (taker.finished) return notOk()
      
      this.putings = this.putings.filter(p => !p.puter.finished)
      if (!this.putings.length) return this.takings.push({taker, ok})
      
      const puting = this.putings.shift()
      if (racing(puting.puter)) puting.puter.finished = true
      puting.ok()
      
      ok(puting.value)
    })
  }
}
pogo(function*() {
  const req = $.get(...)
  while (true) {
    const res = yield race([req, sleep(100)])
    if (res === undefined) console.log('zzz')
    else return res.value
  }
})

Further reading

Check out https://github.com/tj/co and https://github.com/ubolonton/js-csp. I'm working on a library that falls somewhere in the middle here: https://github.com/happy4crazy/pogo. If you'd like to, like, actually run any of this code, check out https://babeljs.io/.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment