Let's explore the concept of 2-way communication using generators with a practical example -
const g = function* (x)
{ // initiating outbound message
// receive inbound message in msg1
const msg1 = yield "sending this out"
// another outbound message
// receive inbound message in msg2
const msg2 = yield "sending this too"
// perform some action
console.log(msg1, msg2)
// finally, return a value
// note: generators can take arguments like x here
return x * 2
}
// create an instance of the generator
const iterator = g(100)
// retrieve the first value from the iterator
let val = iterator.next()
// prepare an example message to send to the generator
let m = 1
while (!val.done)
{ // log each outbound message received
console.log("received message", val.value)
// resume the generator by sending a message back "in"
val = iterator.next(m)
// update the example message
m = m + 1
}
// display the final returned value
console.log("return value", val.value)
Output
received message sending this out
received message sending this too
1 2
return value 200
Understanding 2-way communication through problem-solving is essential. Generators provide pause/resume functionality, making them ideal for asynchronous tasks. While newer methods like async
and await
blend synchronous and asynchronous code seamlessly -
const delay = x =>
new Promise(r => setTimeout(r, 1e3, x))
const main = async (z) =>
{ const x = await delay(200)
const y = await delay(300)
return x + y + z
}
main(100).then(console.log, console.error)
// Result after 2 seconds...
// => 600
Before async
and await
, we utilized generators for 2-way communication as demonstrated below with the run
function. This approach allows us to structure our program similarly but with a generator function and yield
statements -
const delay = x =>
new Promise(r => setTimeout(r, 1e3, x))
const main = function* (z)
{ const x = yield delay(200)
const y = yield delay(300)
return x + y + z
}
const run = it =>
{ const loop = ({ done, value }) =>
done
? Promise.resolve(value)
: value.then(x => loop(it.next(x)))
return loop(it.next())
}
run(main(100)).then(console.log, console.error)
// Result after 2 seconds...
// => 600
In the above snippet, run
recursively processes generator output promises, feeding resolved values back into the generator until completion and providing the final result -
const run = it =>
{ const loop = ({ done, value }) =>
{
if(done) {
return Promise.resolve(value);
} else {
return value.then(x => loop(it.next(x)));
}
}
return loop(it.next());
}
Prior to utilizing generators for mimicking coroutines, crafting asynchronous programs involved manually chaining .then
calls throughout the codebase -
const delay = x =>
new Promise(r => setTimeout(r, 1e3, x))
const main = z =>
delay(200).then(x =>
delay(300).then(y =>
x + y + z
))
main(100).then(console.log, console.error)
// Result after 2 seconds...
// => 600
As demonstrated, leveraging generators for 2-way communication offers clarity and elegance in expressing complex programs. Although JavaScript introduces async
/await
for asynchronous operations, understanding approaches like the run
function provides insights on achieving similar behavior without relying on these new keywords.