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 yield
s.
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 yield
s 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
.
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 bounce
d into the generator with next
, and if it rejects, the rejection gets toss
ed in with throw
. Hopefully the generator eventually returns something, and pogo
promises it.
Types: pogo : (*a | () -> *a) -> promise[a]
.
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
}
})
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/.