What Tamagotchis can teach you about ES6 generators
A presentation at FrontConf in April 2019 in Munich, Germany by Jenn Creighton
What Tamagotchis can teach you about ES6 generators
Hi, I’m Jenn. Frontend Engineer @gurlcode
@gurlcode
SVG @gurlcode
SVG Controls @gurlcode
SVG Canvas Controls @gurlcode
Canvas var canvas = document.querySelector(‘canvas’); var context = canvas.context(‘2d’); context.drawImage(‘tamagotchi.png’, …); @gurlcode
Animations context.drawImage(image, 0, …coordinates); context.clear(); context.drawImage(image, 200, …coordinates); context.clear(); context.drawImage(image, 400, …coordinates);
Animations context.drawImage(image, 0, …coordinates); context.clear(); context.drawImage(image, 200, …coordinates); context.clear(); context.drawImage(image, 400, …coordinates);
Animations context.drawImage(image, 0, …coordinates); context.clear(); context.drawImage(image, 200, …coordinates); context.clear(); context.drawImage(image, 400, …coordinates);
function bounce(x) { return () => { context.drawImage(…, x, …); context.clear(); } } setTimeout(bounce(0), 300); setTimeout(bounce(200), 600); setTimeout(bounce(400), 900); setTimeout(bounce(200), 1200); setTimeout(bounce(0), 1500);
I want to resolve an animation, then handle another animation. var promise = new Promise((resolve, reject) => { …things resolve(value); }; promise().then((value) => …more_things); @gurlcode
Animation with Promises function animate(draw, ms) { return new Promise((resolve, reject) => { var animation = () => { context.clear(); var isComplete = draw(resolve); } } if (!isComplete) { setTimeout(() => (animation), ms); } animation(); }); @gurlcode
Animation with Promises function bounceUp() { var frameWidth = 200; var frameCount = 2; var currentFrame = 0; } return animate((resolve) => { context.drawImage(frameWidth * currentFrame, …); context.clear(); currentFrame++; if (currentFrame > frameCount) { resolve(); return true; } }, ms); @gurlcode
function bounce() { return bounceUp .then(bounceDown); } bounce() .then(bounce); .then(bounce); @gurlcode
function loop() { return idle() .then(loop); } @gurlcode
Except… we got problems @gurlcode
Thenable Hell bounce(() => { moveRight(() => { moveLeft(() => { // Am I in hell? }); }); })
Thenable Hell bounce() bounce(() => { moveRight(() => { moveLeft(() => { // Am I in hell? }); }); }) .then(() => moveRight(20)) .then(() => moveLeft(40)) .then(() => { var shouldFeed = … if () {… else { … }) .then(bounce) .then( // Still in hell. )
var pending = []; function loop() { if (pending.length) { function handleEvent() { return handleEvent(); var event = pending.shift(); } event().then(loop); } return tamagotchi.idle() .then(loop); } @gurlcode
var pending = []; function loop() { if (pending.length) { function handleEvent() { handleEvent(); var event = pending.shift(); } event().then(loop); } return tamagotchi.idle() .then(loop); } @gurlcode
Promises are Unbreakable Vows @gurlcode
I want to pause an animation, & yield to an event. function* generator() { // pause } @gurlcode
Generators 101 @gurlcode
function* count() { yield 1; yield; yield 3; } @gurlcode
function* count() { yield 1; yield; yield 3; } var generator = count(); Generator {}; @gurlcode
function* count() { yield 1; yield; yield 3; } var generator = count(); Generator {}; generator.next(); { value: 1, done: false } @gurlcode
function* count() { yield 1; yield; yield 3; } var generator = count(); Generator {}; generator.next(); { value: 1, done: false } generator.next(); { value: undefined, done: false } generator.next(); { value: 3, done: false } generator.next(); { value: undefined, done: true } @gurlcode
function* count() { yield 1; yield; yield 3; } var generator = count(); Generator {}; generator.next(); { value: 1, done: false } generator.next(); { value: undefined, done: false } generator.next(); { value: 3, done: false } generator.next(); { value: undefined, done: true } @gurlcode
function* count() { yield 1; yield; yield 3; } var generator = count(); Generator {}; generator.next(); { value: 1, done: false } generator.next(); { value: undefined, done: false } generator.next(); { value: 3, done: false } generator.next(); { value: undefined, done: true } @gurlcode
function* add() { const num = yield; yield 2 + num; yield 4 + num; } var generator = add(); Generator {}; generator.next(); { value: undefined, done: false } generator.next(2); { value: 4, done: false } generator.next(); { value: 6, done: false } generator.next(); { value: undefined, done: true } @gurlcode
function* add() { const num = yield; yield 2 + num; yield 4 + num; } var generator = add(); Generator {}; generator.next(); { value: undefined, done: false } generator.next(2); { value: 4, done: false } generator.next(); { value: 6, done: false } generator.next(); { value: undefined, done: true } @gurlcode
function* add() { const num = yield; yield 2 + num; yield 4 + num; } var generator = add(); Generator {}; generator.next(); { value: undefined, done: false } generator.next(2); { value: 4, done: false } generator.next(); { value: 6, done: false } generator.next(); { value: undefined, done: true } @gurlcode
function* add() { const num = yield; yield 2 + num; yield 4 + num; } var generator = add(); Generator {}; generator.next(); { value: undefined, done: false } generator.next(2); { value: 4, done: false } generator.next(); { value: 6, done: false } generator.next(); { value: undefined, done: true } @gurlcode
Infinite Generators @gurlcode
function* forever(num) { while (true) { yield 2 + num; } } var generator = forever(2); Generator {}; @gurlcode
function* forever(num) { while (true) { num = yield 2 + num; } } var generator = forever(2); Generator {}; generator.next(); { value: 4, done: false } generator.next(2); { value: 6, done: false } generator.next(2); { value: 8, done: false } generator.next(2); { value: 10, done: false } { value: …, done: false } @gurlcode
yield* @gurlcode
function* outer() { yield 1; yield* inner(); yield 2; } function* inner() { yield “a”; yield “b”; } var generator = outer(); Generator {}; @gurlcode
function* outer() { yield 1; yield* inner(); yield 2; } function* inner() { yield “a”; yield “b”; } var generator = outer(); Generator {}; @gurlcode
function* outer() { yield 1; yield* inner(); yield 2; } function* inner() { yield “a”; yield “b”; } var generator = outer(); Generator {}; @gurlcode
function* outer() { yield 1; yield* inner(); yield 2; } function* inner() { yield “a”; yield “b”; } var generator = outer(); Generator {}; @gurlcode
function* outer() { yield 1; yield* inner(); yield 2; } function* inner() { yield “a”; yield “b”; } generator.next(); { value: 1, done: false } generator.next(); { value: “a”, done: false } generator.next(); { value: “b”, done: false } generator.next(); { value: 2, done: false } generator.next(); { value: undefined, done: true }
function* outer() { yield 1; yield* inner(); yield 2; } function* inner() { yield “a”; yield “b”; } generator.next(); { value: 1, done: false } generator.next(); { value: “a”, done: false } generator.next(); { value: “b”, done: false } generator.next(); { value: 2, done: false } generator.next(); { value: undefined, done: true }
function* outer() { yield 1; yield* inner(); yield 2; } function* inner() { yield “a”; yield “b”; } generator.next(); { value: 1, done: false } generator.next(); { value: “a”, done: false } generator.next(); { value: “b”, done: false } generator.next(); { value: 2, done: false } generator.next(); { value: undefined, done: true }
function* outer() { yield 1; yield* inner(); yield 2; } function* inner() { yield “a”; yield “b”; } generator.next(); { value: 1, done: false } generator.next(); { value: “a”, done: false } generator.next(); { value: “b”, done: false } generator.next(); { value: 2, done: false } generator.next(); { value: undefined, done: true }
Canceling a Generator @gurlcode
function* forever(num) { while (true) { yield 2 + num; return; } } var generator = forever(2); Generator {}; generator.next(); { value: 4, done: false } generator.next(); { value: undefined, done: true } generator.next(); { value: undefined, done: true } @gurlcode
function* forever(num) { while (true) { yield 2 + num; return; } } var generator = forever(2); Generator {}; generator.next(); { value: 4, done: false } generator.next(); { value: undefined, done: true } generator.next(); { value: undefined, done: true } @gurlcode
function* forever(num) { while (true) { yield 2 + num; return; } } var generator = forever(2); Generator {}; generator.next(); { value: 4, done: false } generator.next(); { value: undefined, done: true } generator.next(); { value: undefined, done: true } @gurlcode
function* forever(num) { while (true) { yield 2 + num; } } var generator = forever(2); Generator {}; generator.next(); { value: 4, done: false } generator.return(); { value: undefined, done: true } @gurlcode
function* forever(num) { while (true) { yield 2 + num; } } var generator = forever(2); Generator {}; generator.next(); { value: 4, done: false } generator.return(); { value: undefined, done: true } @gurlcode
Resuming the generator occurs outside of the generator. function* generatorFunc() { yield 1; yield 2; } var generator = generatorFunc(); generator.next(); @gurlcode
s n u r s r o t a r e n e g @gurlcode
Coroutines! @gurlcode
Coroutines are a general control structure whereby control flow is cooperatively passed between two different routines. @gurlcode
function coroutine(generatorFunc) { const generator = generatorFunc(); nextResponse(); function nextResponse(value) { const response = generator.next(value); if (response.done) { return; } nextResponse(response.value); } } @gurlcode
function coroutine(generatorFunc) { const generator = generatorFunc(); nextResponse(); function nextResponse(value) { const response = generator.next(value); if (response.done) { return; } nextResponse(response.value); } } @gurlcode
function coroutine(generatorFunc) { const generator = generatorFunc(); nextResponse(); function nextResponse(value) { const response = generator.next(value); if (response.done) { return; } nextResponse(response.value); } } @gurlcode
function* bounceUp() { yield context.drawImage(…); yield context.drawImage(…); yield context.drawImage(…); // …etc } coroutine(bounceUp); @gurlcode
function* bounceUp() { yield new Promise(…); yield new Promise(…); yield new Promise(…); // …etc } coroutine(bounceUp); @gurlcode
function nextResponse(value) { const response = generator.next(value); if (response.done) { return; } handleAsync(response.value); } @gurlcode
function handleAsync(value) { value.then(nextResponse); } @gurlcode
coroutine(function* bounce() { yield bounceUp; yield bounceDown; }); @gurlcode
co(function* () { yield promise(); yield (callback) => { // do some things callback() }; yield generatorFunction; }); https://github.com/tj/co @gurlcode
You can think sequentially about async code. function* idle() { yield* bounce; yield* moveRight; yield* moveLeft; yield* bounce; } @gurlcode
@gurlcode
function delay(ms) { return new Promise((resolve) => setTimeout(() => resolve(), ms)); } function clear() { context.clear(…); } function draw(image, frame, …) { context.drawImage(image, frame, …); } @gurlcode
function delay(ms) { return new Promise((resolve) => setTimeout(() => resolve(), ms)); } function clear() { context.clear(…); } function draw(image, frame, …) { context.drawImage(image, frame, …); } @gurlcode
function delay(ms) { return new Promise((resolve) => setTimeout(() => resolve(), ms)); } function clear() { context.clear(…); } function draw(image, frame, …) { context.drawImage(image, frame, …); } @gurlcode
function* drawFrame(image, frame, ms = 100) { clear(); draw(image, frame); yield delay(ms); } function* dislike() { yield* drawFrame(‘dislike’, 0); yield* drawFrame(‘dislike’, 1); } @gurlcode
function* drawFrame(image, frame, ms = 100) { clear(); draw(image, frame); yield delay(ms); } function* dislike() { yield* drawFrame(‘dislike’, 0); yield* drawFrame(‘dislike’, 1); } @gurlcode
@gurlcode
coroutine(function* loop() { let done; let animation = idle(); while (true) { if (pending) { animation.return(); yield* event(); } while (!done && !pending) { const next = animation.next(); done = next.done; yield next.value; } animation = idle(); done = false; } }); @gurlcode
coroutine(function* loop() { let done; let animation = idle(); while (true) { if (pending) { animation.return(); yield* event(); } while (!done && !pending) { const next = animation.next(); done = next.done; yield next.value; } animation = idle(); done = false; } }); @gurlcode
coroutine(function* loop() { let done; let animation = idle(); while (true) { if (pending) { animation.return(); yield* event(); } while (!done && !pending) { const next = animation.next(); done = next.done; yield next.value; } animation = idle(); done = false; } }); @gurlcode
coroutine(function* loop() { let done; let animation = idle(); while (true) { if (pending) { animation.return(); yield* event(); } while (!done && !pending) { const next = animation.next(); done = next.done; yield next.value; } animation = idle(); done = false; } }); @gurlcode
function* feed() { if (hunger === 0) { yield* dislike(); } } function* dislike() { yield* drawFrame(‘dislike’, 0); yield* drawFrame(‘dislike’, 1); } hunger—-; yield* eat(); @gurlcode
function* feed() { if (hunger === 0) { yield* dislike(); } } hunger—-; yield* eat(); function* eat() { yield* drawFrame(‘eat’, 0); yield* drawFrame(‘eat’, 1); } @gurlcode
@gurlcode
Thank you! https://github.com/jcreighton/tamagotchi @gurlcode