Skip to content

Instantly share code, notes, and snippets.

@joelburget
Forked from cqfd/generators.md
Last active August 29, 2015 14:23
Show Gist options
  • Save joelburget/57a41579ffdc2a704df5 to your computer and use it in GitHub Desktop.
Save joelburget/57a41579ffdc2a704df5 to your computer and use it in GitHub Desktop.

Generators

Let's look at an example:

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 start 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. We can unstick her with next:

Here I'm confused. There are three calls to next for two yields? Control flow goes:

next -> console.log(Hi! I'm ${name} and I'm your teammate.) const num = yield "I need a number!"

next -> const num2 = yield "I need another number!"

next -> console.log("Ok, here's my work.") return num * num2

IE each next runs until a yield, starting from the beginning? Like slices of bread, you get one more next than yield?

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

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!'

Yikes, there's a lot going on here. Wouldn't mind being walked through it in more detail.

Values we pass in 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

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* getMessages(since) {
  while (true) {
    const msgs = yield $.getJSON('/messages', { since })
    what's messages.length?
    if (msgs.length) return msgs
    yield sleep(5000)
  }
}

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

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. Presumably the generator will eventually return something, and pogo promises it.

Diagrams. A diagram of the control flow bouncing around would be brilliant here.

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)

I feel like this could be two or three posts. I got a little bogged down midway through. And obviously I need some more guidance on this part.

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