├── deck.html ├── deck.md ├── deno.json ├── deno.lock ├── ex ├── async.ts ├── callback.ts ├── compose.ts ├── counter.ts ├── create-template.ts ├── deps.ts ├── starfx.ts └── yield-delegate.ts └── img ├── aptible.svg ├── arrow.jpg ├── calc.png ├── callback-hell.jpg ├── confused.jpg ├── db_vader.jpg ├── qrcode.png ├── starfx.png ├── tree.png └── yellow-to-lavender.svg /deck.html: -------------------------------------------------------------------------------- 1 |
13 |

Delimited continuations are all you need

14 |
15 |
16 |

Flow control is how imperative programs make a choice about what code is to
17 | be run.^1

18 |
19 |
20 |

Async flow control is the same definition but using asynchronous constructs.

21 |
22 |
23 |

There a lot of familiar paradigms we can employ in Typescript:

24 |
    25 |
  • callbacks
  • 26 |
  • promises
  • 27 |
  • generators
  • 28 |
  • observables
  • 29 |
  • async/await
  • 30 |
31 |
32 |
33 |

Why are callbacks within callbacks a bad paradigm?

34 |
35 |
36 |

You'll read countless articles about how great async/await is and everyone
37 | should use it.

38 |

However, can we think of any downsides to using async/await?

39 |
40 |
41 |

Downsides of async/await

42 |
    43 |
  • Everything is a promise
  • 44 |
  • Everything is now async
  • 45 |
  • The browser controls the flow of execution for async/await
  • 46 |
  • Unhandled promise rejections are pernicious
  • 47 |
48 |
49 |
50 |

What if I told you there was a better way to express async flow control?

51 |
52 |
53 |

Detour: What color is your function?^2

54 |
55 |
56 |

Did you know functions have a color?

57 |
    58 |
  • function() {} -> blue
  • 59 |
  • async function() {} -> red
  • 60 |
  • function*() {} -> green
  • 61 |
  • async function*() {} -> yellow
  • 62 |
63 |
64 |
65 |
    66 |
  • function() {} -> blue
  • 67 |
  • async function() {} -> red
  • 68 |
69 |

There's no problem with blue functions calling other blue functions: you'll
70 | get a return value synchronously.

71 |

There's also no problem with red functions calling blue functions: you'll
72 | still get a promise that you can await.

73 |

However, what happens when you call a red function inside a blue one?

74 |
75 |
76 |

It's easy enough to just make every function red and then you await it
77 | inside the initial script:

78 |
async function init() {
 79 |   // do some flow control
 80 | }
 81 | 
 82 | init().then(console.log).catch(console.error);
 83 | 
84 |
85 |
86 |

That's all fine and good but it has real implications to how the code functions.

87 |

You are now living in the world where everything is a promise.

88 |
89 |
90 |

Promises are great, right?

91 |

Questions to think about:

92 |
    93 |
  • How do I compose promises to express flow control? (e.g. parallel, race, etc.)
  • 94 |
  • How do I handle promise errors?
  • 95 |
  • What happens if I forget to .catch() every promise?
  • 96 |
  • How can I cancel a promise?
  • 97 |
98 |

Finally, the JS engine resolves a promise via a microtask.

99 |
100 |
101 |
    102 |
  • macrotasks: 103 |
      104 |
    • setTimeout
    • 105 |
    • setInterval
    • 106 |
    • setImmediate
    • 107 |
    • requestAnimationFrame
    • 108 |
    • UI rendering
    • 109 |
    110 |
  • 111 |
  • microtasks: 112 |
      113 |
    • Promise
    • 114 |
    • process.nextTick
    • 115 |
    • queueMicrotask
    • 116 |
    • MutationObserver
    • 117 |
    118 |
  • 119 |
120 |

Read more about microtasks.^3

121 |
122 |
123 |

What are delimited continuations (DC)?

124 |
    125 |
  • It's all about flow control 126 |
      127 |
    • Doesn't matter if it's sync or async, DCs look the same
    • 128 |
    • DC is the abstraction upon which all flow control paradigms can be built
    • 129 |
    130 |
  • 131 |
  • continuation: The rest of code execution reified (A) inside a function
  • 132 |
  • delimited: The reified (A) function can return a value -- which allows
    133 | for composition
  • 134 |
  • Two primitives -- shift() and reset() -- are all you need for complex
    135 | async flow control
  • 136 |
137 |
138 |
139 |

Detour: Yield delegates

140 |

Typescript has basic support for generators but not for the type of constructs
141 | we are building.^4

142 |
function* fun() {
143 |   return number;
144 | }
145 | 
146 | function* raw() {
147 |   const value = yield fun(); // value = any
148 | }
149 | 
150 |
151 |
152 |

Yield delegates yield * provide a way to reach into the generator and have it
153 | output the proper type.

154 |
function* fun() {
155 |   return number;
156 | }
157 | 
158 | function* typed() {
159 |   const value = yield* fun(); // value = number
160 | }
161 | 
162 |

Now we can use some clever tricks to get the proper types out of our yield
163 | statements.

164 |
165 |
166 |

Code examples

167 |

Using continuation^5

168 |
169 |
170 |

Sync example

171 |

The first example I would like to show is one of simple string concatenation.

172 |
173 |
174 |

The entire flow of the evaluate() function is synchronous in this example and
175 | as such we can use it like a normal sync function.

176 |
import { Computation, shift, evaluate } from './deps.ts';
177 | 
178 | function slot<T>(): Computation<T> {
179 |   return shift<T>(function* (k) {
180 |     return k;
181 |   });
182 | }
183 | 
184 | function dc() {
185 |   return evaluate<(s: string) => (s: string) => string>(function* () {
186 |     const greeting = yield* slot<string>();
187 |     const thing = yield* slot<string>();
188 |     return `${greeting}, ${thing}!`;
189 |   });
190 | }
191 | 
192 | const tmpl = dc();
193 | const result = tmpl('hello')('world');
194 | console.log(result); // hello, world!
195 | 
196 |
197 |
198 |

key points

199 |
    200 |
  • k is the continuation "callback"
  • 201 |
  • When you return k from a shift, the evaluate function returns that k
    202 | function as its value
  • 203 |
  • Inside the evaluate function greeting and thing are the values passed into
    204 | it from outside the evaluate body function
  • 205 |
  • shift() can be thought of as slots that eventually get filled by values
    206 | passed into evaluate()!
  • 207 |
208 |
209 |
210 |
211 |
function dc() {
212 |   return (left: string) => {
213 |     return (right: string) => {
214 |       return `${left}, ${right}!`;
215 |     };
216 |   };
217 | }
218 | 
219 | const tmpl = dc();
220 | const result = tmpl('hello')('world');
221 | console.log(result); // hello, world!
222 | 
223 |
224 |
225 |

Continuation-passing style^6

226 |
function cps(cont: (s: string) => void) {
227 |   return (left: string) => {
228 |     return (right: string) => {
229 |       cont(`${left}, ${right}!`);
230 |     };
231 |   };
232 | }
233 | 
234 | const tmpl = cps((result) => {
235 |   console.log(result); // hello, world!
236 | });
237 | tmpl('hello')('world');
238 | 
239 |
240 |
241 |

Async example

242 |

In the next example, I would like to show how we can incorporate async flow
243 | control.

244 |
import { evaluate, shift } from './deps.ts';
245 | 
246 | const run = evaluate<(n: number) => Promise<number>>(function* () {
247 |   const left = yield* shift(function* (k) {
248 |     return k;
249 |   });
250 | 
251 |   const right = yield* shift(function* (resolve) {
252 |     return Promise.resolve(55).then(resolve);
253 |   });
254 | 
255 |   return left + right;
256 | });
257 | 
258 | const result = await run(13);
259 | console.log(result); // 68
260 | 
261 |
262 |
263 |

key points

264 |
    265 |
  • It doesn't matter if it's sync or async, delimited continuations can handle
    266 | that flow control
  • 267 |
  • The right shift returns a promise which is what gets returned when calling
    268 | run(13)
  • 269 |
  • Since the return value is a promise, we await the answer
  • 270 |
  • I renamed the shift k variable to resolve to demonstrate how similar it
    271 | is to a promise "continuation."
  • 272 |
  • When you use a promise, it will always be async 273 |
      274 |
    • When you use a delimited continuation, it might be async
    • 275 |
    276 |
  • 277 |
  • It doesn't matter if the flow of code execution is sync or async, delimited
    278 | continuations handle them the exact same
  • 279 |
280 |
281 |
282 |

Composition example

283 |
import { shift, evaluate, Computation, Continuation } from "./deps.ts";
284 | 
285 | type ShiftProp<T = unknown> = (
286 |   res: Continuation<T>,
287 |   rej?: Continuation<Error>
288 | ) => Computation;
289 | 
290 | function* add(
291 |   lhs: ShiftProp<number>,
292 |   rhs: ShiftProp<number>
293 | ): Computation<number> {
294 |   const left = yield* shift(lhs);
295 |   const right = yield* shift(rhs);
296 |   return left + right;
297 | }
298 | 
299 | const sync = (value: number) =>
300 |   function* (k: Continuation<number>) {
301 |     return k(value);
302 |   };
303 | 
304 | const ev = evaluate(function* () {
305 |   const first = yield* add(sync(13), function* (k) {
306 |     return Promise.resolve(55).then(k);
307 |   });
308 | 
309 |   const second = yield* add(
310 |     function* (k) {
311 |       setTimeout(() => k(21), 1000);
312 |     },
313 |     function* (k) {
314 |       k(Math.random());
315 |     }
316 |   );
317 | 
318 |   const result = yield* add(sync(first), sync(second));
319 |   console.log(result);
320 |   return result;
321 | });
322 | 
323 | console.log(ev);
324 | 
325 |
326 |
327 |

Counter example

328 |
interface Count {
329 |   value: number;
330 |   increment(): Count;
331 | }
332 | 
333 | function useCounter() {
334 |   return reset<Count>(function* () {
335 |     for (let i = 0; ; i++) {
336 |       yield* shift<void>(function* (k) {
337 |         return { value: i, increment: k };
338 |       });
339 |     }
340 |   });
341 | }
342 | 
343 | let start = evaluate<Count>(useCounter);
344 | 
345 | let once = start.increment();
346 | let twice = once.increment();
347 | let thrice = twice.increment();
348 | 
349 | console.dir([once.value, twice.value, thrice.value]); // [1, 2, 3]
350 | 
351 |
352 |
353 |

wat

354 |
355 |
356 |

wat

357 |
358 |
359 |

I'm still very confused by the coding paradigm. Everytime I look at this code I
360 | get a headache.

361 |

Most end-developers aren't going to be using delimited continuations directly.

362 |

Rather, this tool will allow library developers to build on top of it.

363 |
364 |
365 |

effection v3^7

366 |

effection takes continuation and builds a task tree. All tasks are cleaned
367 | up automatically via a set of cancellation strategies:

368 |
    369 |
  • tasks spawn other tasks
  • 370 |
  • tasks can be halt()ed 371 |
      372 |
    • All descendants are halt()ed
    • 373 |
    • All ancestors are halt()ed
    • 374 |
    375 |
  • 376 |
377 |
378 |
379 |

effection also has higher level compositions of shift() and reset() which
380 | grants us a flourish of functionality. For example:

381 |
    382 |
  • suspend(): permenantly suspend a generator function at a yield point.
  • 383 |
  • action(): which is a wrapper for shift() with proper cleanup of the task
  • 384 |
  • spawn(): creates a sub-task
  • 385 |
  • createChannel(): kind of like an event emitter but using DC
  • 386 |
  • sleep(n): temporarily suspend generator function for for (n) milliseconds
  • 387 |
388 |
389 |
390 |

I still don't get it

391 |
392 |
393 |

Building on top of effection we now have a set of middle-level primitives that
394 | allow us to build any flow control paradigm we want.

395 |
396 |
397 |

Jake Archibald recently wrote an interesting article about unhandled
398 | rejections.^8

399 |

In it he talks about the use case of wanting to fetch book chapters in parallel
400 | but process them in sequence:

401 |
async function showChapters(chapterURLs) {
402 |   const chapterPromises = chapterURLs.map(async (url) => {
403 |     const response = await fetch(url);
404 |     return response.json();
405 |   });
406 | 
407 |   for await (const chapterData of chapterPromises) {
408 |     appendChapter(chapterData);
409 |   }
410 | }
411 | 
412 |
413 |
414 |

He goes on to describe the the potential for bugs because of
415 | unhandled rejections. Below is the "final" solution.

416 |
async function showChapters(chapterURLs) {
417 |   const chapterPromises = chapterURLs.map(async (url) => {
418 |     const response = await fetch(url);
419 |     return response.json();
420 |   });
421 | 
422 | + for (const promise of chapterPromises) promise.catch(() => {});
423 | 
424 |   for await (const chapterData of chapterPromises) {
425 |     appendChapter(chapterData);
426 |   }
427 | }
428 | 
429 |
430 |
431 |

starfx^9

432 |
433 |
434 |

Using starfx^9 we could do something like this:

435 |
import { request, json, parallel, forEach } from 'starfx';
436 | 
437 | function* showChapters(chapterURLs: string[]) {
438 |   const reqs = chapterURLs.map(function (url) {
439 |     return function* () {
440 |       const response = yield* request(url);
441 |       return yield* json(response);
442 |     };
443 |   });
444 | 
445 |   const chapters = yield* parallel(reqs);
446 | 
447 |   yield* forEach(chapters.sequence, function* (chapter) {
448 |     if (chapter.ok) {
449 |       appendChapter(chapter.value);
450 |     } else {
451 |       console.error(chapter.error);
452 |     }
453 |   });
454 | }
455 | 
456 |
457 |
458 |

No uncaught exceptions, code is just as simple to understand.

459 |

Further, we automatically pass an AbortController.signal to all http requests
460 | because of the request fx we wrote.

461 |

When a task is halt()ed or crashes, we trigger the signal to abort the fetch
462 | call.

463 |

Automatic cleanup!

464 |
465 |
466 |

We are just getting started

467 | 468 |

Things we plan on building:

469 |
    470 |
  • Inspector / debugger for effection
  • 471 |
  • Side-effect system for redux (ala redux-saga)
  • 472 |
  • Query and cache management (ala react-query)
  • 473 |
  • Web server (ala express)
  • 474 |
  • View library (ala react)
  • 475 |
476 |
477 |
478 |

aptible logo ^10

479 |

The most successful PaaS you didn't know existed.

480 |

https://aptible.com

481 |
482 |
483 |

The Frontside^11

484 |

https://frontside.com

485 |

https://discord.gg/frontside

486 |
487 |
488 |

fin

489 |
490 |

Hello, everyone! 492 | 493 | I hope you all have your thinking caps on because this is going require your full attention. 494 | 495 | In this presentation I'm going to show you a brand new paradigm for handling 496 | async flow control in your code. 497 | 498 | So buckle up, because we have a lot of ground to cover, and it's going to be a wild ride.

Let's start by asking a simple question. 499 | 500 | What is flow control? 501 | 502 | Flow **control** is how imperative programs make a choice about what code is to 503 | be run. 504 | 505 | I want to emphasize that control doesn't just mean the visual design of the source 506 | code ... but also how much control **you** the end-developer has over the tools you 507 | can leverage. 508 | 509 | This is a subtle but important distinction that will be demonstrated throughout 510 | this presentation.

Let's continue. 511 | 512 | What is async flow control? 513 | 514 | Pause for effect. Count to 5. 515 | 516 | Async flow control is the same definition but using asynchronous constructs. 517 | 518 | I hope you are now recalling all the async constructs you know about. 519 | 520 | I'll give you a second to think about them in your head. 521 | 522 | Pause for effect. Count to 5.

I'm sure at some point in time we've employed all of these constructs to 523 | express async flow control ... with varying degress of success. 524 | 525 | However, some are considered by the dev community better than others. 526 | 527 | Can you rank-order these async constructs in your head from worst-to-best? 528 | 529 | Pause for effect. Count to 5. 530 | 531 | Callbacks are regarded as less-than-ideal. We've all heard the term "callback 532 | hell" and do everything we can to avoid falling into the trap of callbacks 533 | within callbacks within callbacks.

Why are callbacks within callbacks a bad paradigm? 534 | 535 | Pause for effect. Count to 5. 536 | 537 | What about the image on the right is less-than-ideal? 538 | 539 | Pause for effect. Count to 5. 540 | 541 | The zen of python dictates that "flat is better than nested" ... but why? 542 | 543 | When thinking about the visual design of code, there are a couple of features 544 | we care about: 545 | 546 | - Readability 547 | - Maintainability 548 | - The likelyhood of missing an error 549 | 550 | These are mission critical features of our code that require our thoughtful 551 | consideration. It is **imperative** that we are able to understand the code that 552 | we are writing ... and possibly more importantly, reading. 553 | 554 | Can you think of any other features we should care about? 555 | 556 | Pause for effect. Count to 5.

You'll read countless articles about how great `async`/`await` is and everyone 557 | should use it. 558 | 559 | You probably ranked `async`/`await` pretty high on your list, didn't you? 560 | 561 | Pause for effect. Count to 5. 562 | 563 | However, can we think of any downsides to using `async`/`await`? 564 | 565 | Pause for effect. Count to 5.

Here's a list of downsides I could think of quickly. 566 | 567 | Pause for effect. Count to 5. 568 | 569 | You're probably thinking: wait, I don't understand, some of these "downsides" 570 | are things that have never negatively impacted me. 571 | 572 | Trust your intuition.

But what if I told you there was a better way to express `async` flow control? 573 | 574 | Would you believe me? 575 | 576 | Pause for effect. Count to 5. 577 | 578 | But first, a detour.

Did you know functions have a color? 579 | 580 | Huh?! Excuse me? 581 | 582 | Pause for effect. Count to 3. 583 | 584 | Yes! Not all functions are created equal. They are not always interchangable. 585 | 586 | Functions behave differently depending on their **color**.

There's no problem with `blue` functions calling other `blue` functions: you'll 587 | get a return value synchronously. 588 | 589 | There's also no problem with `red` functions calling `blue` functions: you'll 590 | still get a promise that you can `await`. 591 | 592 | However, what happens when you call a `red` function inside a `blue` one? 593 | 594 | Pause for effect. Count to 3. 595 | 596 | What I'm really trying to demonstrate here is kind of subtle ... 597 | which is async/await **forces** our entire flow control to be async by default. 598 | 599 | You **must** always await the result of a `red` function. 600 | 601 | Don't worry, this is all leading to delimited continuations and why they are so interesting.

It's easy enough to just make every function `red` and then you `await` 602 | it inside the initial script ... right? 603 | 604 | So, what's the problem?

That's all fine and good but it has real implications to how the code functions. 605 | 606 | You are now living in the world where everything is a **promise**. 607 | 608 | Pause for effect. 609 | 610 | Ahhh, this is interesting. 611 | 612 | I want you to think about everything being a promise and with it all the pros and cons. 613 | 614 | Why would everything being a promise, be an issue?

Promises are great, right? 615 | 616 | Yes, they are! I use promises all the time, but there are a lot of questions to 617 | ask yourself when using them. 618 | 619 | Pause for effect. Count to 5. 620 | 621 | Finally, the JS engine resolves a promise via a **microtask**. 622 | 623 | Oh this is interesting. 624 | 625 | We've found a rabbit hole ... but here are some questions to ask yourself: 626 | 627 | - What is a microtask? 628 | - Why do I care that a Promise is a microtask? 629 | - What does that mean for the flow control of my code? 630 | 631 | I'm not going to answer these questions today, but rather ask you all to 632 | investigate further. 633 | 634 | Instead I'll just list some examples of micro- and macro-tasks

Here's the list. 635 | 636 | Pause for effect. Count to 5.

Finally! We made it! 637 | 638 | So what are delimited continuations? 639 | 640 | I'm going to try to explain delimited continuations and fail. 641 | 642 | This is something that I'm still struggling to grok as someone who is actively 643 | using them. I don't have all the answers and I'm **not** the expert. 644 | 645 | But that's okay, this presentation is designed to be an introduction and prompt you 646 | all to learn more. 647 | 648 | Before we go any further, I must make a detour.

Typescript has basic support for generators but not for the type of constructs 649 | we are building. 650 | 651 | This ... is a big problem. If we can't leverage typescript to resolve types 652 | properly we lose a ton of value in using delimited continuations as the 653 | foundation for async flow control. 654 | 655 | However, there's a hack and it's called yield delegates.

Yield delegates (the `yield *` syntax) provides a way to reach into the generator and have it output the proper type for us. 656 | 657 | It's a hack to be sure, but it works very well for our use-case.

Finally, you might be thinking, some code! 658 | 659 | This is where things get weird. 660 | 661 | Continuation is a library built by the team at frontside where most of the 662 | experimentation is happening.

As you can see at the bottom, we are doing nothing fancy. We are simply 663 | combining two strings together using a template function. 664 | 665 | You'll see references to `k` a lot in these examples. That is the continuation 666 | function. When called, it continues the yield from the shift onto the next 667 | yield statement. It is the continuation mechanism. Think of is like a 668 | callback.

Are we having fun yet? 669 | 670 | Laugh manically.

Let's take a step back for a second. How could we implement something similar 671 | using plain `blue` functions? 672 | 673 | We have a function return a function return a function. 674 | 675 | This is a variant of callback hell, it's not ideal, but it works for the example we 676 | demonstrated previously.

Let's try this again but using a coninuation-passing style `blue` function. 677 | 678 | Here we receive the templated string as a callback to our `cps` function. 679 | 680 | This is also a variant of callback hell. Not great and in this example 681 | unnecessary. 682 | 683 | But, you can imagine when you start adding `red` functions here that it will 684 | eventually become necessary. 685 | 686 | Let's continue.

Back to delimited continuations. Except this time, we are going to mix some 687 | `red` functions into our flow control.

Using deliminited continuations, we can compose `shift()` to do some complex 688 | flow control. 689 | 690 | I'd love to focus on this example, however, for the sake of preventing our 691 | collective **minds** from exploding, I'll spare us the details and briefly 692 | summarize. 693 | 694 | - Here we are adding a bunch of numbers together 695 | - Some numbers are resolved syncronously and some are async 696 | - The key idea I want to illustrate is that composition using `shift()` is not 697 | only possible but relatively flat and easy to do 698 | 699 | Please feel free to grab the github repo for this slide deck and run this 700 | example at your leisure.

Finally, I want to demonstrate computation in a bottle which is what `reset()` 701 | is all about. 702 | 703 | Here we are using `reset()` to make the counter reusable. 704 | 705 | Now we can call `useCounter` as many times as we want in order to create new 706 | instances of our counter. 707 | 708 | ```ts 709 | evaluate(function*() { 710 | let x = yield* useCounter(); 711 | let y = yield* useCounter(); 712 | }); 713 | ``` 714 | 715 | I'm going to stop there. There is so much more to learn about this paradigm 716 | but unfortunately we have to move on.

It's time to go higher level.

I know what you're thinking. 717 | 718 | This is a lot to handle. 719 | 720 | I'd like to take a second for us all to take a deep breathe because we aren't 721 | done yet. 722 | 723 | Pause for effect. Count to 5. 724 | 725 | Ok, here we go

Building on top of `effection` we now have a set of middle-level primitives that 726 | allow us to build any flow control paradigm we want. 727 | 728 | Using suspend, action, spawn, createChannel, and sleep we have everything we 729 | need to express all forms of async flow control. This is the one paradigm to 730 | rule all paradigms. 731 | 732 | I know it probably hasn't clicked yet, and I'm probably not doing the best job 733 | explaining its full potential. 734 | 735 | But give it time, stay engaged, join the discord where we are actively 736 | developing this technology.

Here is a function that fetches all book chapters in parallel but processes 737 | them in sequence using `for await ... of`. 738 | 739 | Pretty cool. 740 | 741 | Pause for effect. Count to 5.

The key point I want to make here is the end-user needs to be thoughtful about 742 | the promises they activate while at the same time leveraging the JS engine to 743 | handle async flow control for them. 744 | 745 | This is tough because there are no signals -- not even in typescript -- that 746 | aid you in detecting unhandle promise rejections. 747 | 748 | Ouch.

Let's go higher level. 749 | 750 | `starfx` builds off of effection in order to be used inside the browser. 751 | 752 | It is an experimental library that could eventually supersede redux-saga as 753 | well as react-query and more.

Here we use a couple APIs from `starfx` to do the same thing but with a bunch 754 | of safety mechanisms built in. 755 | 756 | `request` is a wrapper around `fetch` that will automatically abort when a task 757 | is `halt()`ed. 758 | 759 | `parallel` is a function to call all functions it receives at the same time and 760 | returns a channel that the user can loop over in sequence -- or even as they become 761 | available. It's dealers choice. 762 | 763 | Further, `parallel` will never throw, it automatically catches errors, and returns 764 | a `Result` for each request. 765 | 766 | `Result` is inspired by rust's `Result` type which is highly regarded. 767 | 768 | To be clear, this is a proposed API, we are stilling working on the initial 769 | implementation.

So what do we have? 770 | 771 | Read slide. 772 | 773 | We are not restricted to the async flow control `Promise` and `async`/`await` provides. 774 | 775 | We are able to create as many different flow control structures as we want, all leveraging delimited continuations.

We are just getting started. 776 | 777 | This is an area of active development and a foundation on which we can build 778 | any async flow control constructs. 779 | 780 | I don't know if it has clicked for anyone else, but it has pretty much consumed 781 | all my free time and energy.

I wanted to give a quick shout out to Aptible, the most successful PaaS you 782 | didn't know existed. 783 | 784 | I've been working there for 4+ years. 785 | 786 | Please feel free to contact me or aptible if you are interested in learning 787 | more.

Also a shout out to the team at Frontside who created most of the paradigms and 788 | libraries I've been talking about today.

-------------------------------------------------------------------------------- /deck.md: -------------------------------------------------------------------------------- 1 | --- 2 | paginate: true 3 | --- 4 | 5 | # Delimited continuations are all you need 6 | 7 | ![bg right contain](./img/qrcode.png) 8 | 9 | 19 | 20 | --- 21 | 22 | Flow **control** is how imperative programs make a choice about what code is to 23 | be run.[^1] 24 | 25 | 40 | 41 | --- 42 | 43 | Async flow control is the same definition but using asynchronous constructs. 44 | 45 | 60 | 61 | --- 62 | 63 | There a lot of familiar paradigms we can employ in Typescript: 64 | 65 | - callbacks 66 | - promises 67 | - generators 68 | - observables 69 | - `async`/`await` 70 | 71 | 85 | 86 | --- 87 | 88 | # Why are callbacks within callbacks a bad paradigm? 89 | 90 | ![bg right:66% contain](./img/callback-hell.jpg) 91 | 92 | 118 | 119 | --- 120 | 121 | You'll read countless articles about how great `async`/`await` is and everyone 122 | should use it. 123 | 124 | However, can we think of any downsides to using `async`/`await`? 125 | 126 | 138 | 139 | --- 140 | 141 | # Downsides of `async`/`await` 142 | 143 | - Everything is a promise 144 | - Everything is now async 145 | - The browser controls the flow of execution for `async`/`await` 146 | - Unhandled promise rejections are pernicious 147 | 148 | 158 | 159 | --- 160 | 161 | What if I told you there was a better way to express `async` flow control? 162 | 163 | 172 | 173 | --- 174 | 175 | # Detour: What color is your function?[^2] 176 | 177 | --- 178 | 179 | Did you know functions have a color? 180 | 181 | - `function() {}` -> blue 182 | - `async function() {}` -> red 183 | - `function*() {}` -> green 184 | - `async function*() {}` -> yellow 185 | 186 | 197 | 198 | --- 199 | 200 | - `function() {}` -> `blue` 201 | - `async function() {}` -> `red` 202 | 203 | There's no problem with `blue` functions calling other `blue` functions: you'll 204 | get a return value synchronously. 205 | 206 | There's also no problem with `red` functions calling `blue` functions: you'll 207 | still get a promise that you can `await`. 208 | 209 | However, what happens when you call a `red` function inside a `blue` one? 210 | 211 | 229 | 230 | --- 231 | 232 | It's easy enough to just make every function `red` and then you `await` it 233 | inside the initial script: 234 | 235 | ```ts 236 | async function init() { 237 | // do some flow control 238 | } 239 | 240 | init().then(console.log).catch(console.error); 241 | ``` 242 | 243 | 249 | 250 | --- 251 | 252 | That's all fine and good but it has real implications to how the code functions. 253 | 254 | You are now living in the world where everything is a **promise**. 255 | 256 | 269 | 270 | --- 271 | 272 | Promises are great, right? 273 | 274 | Questions to think about: 275 | 276 | - How do I compose promises to express flow control? (e.g. parallel, race, etc.) 277 | - How do I handle promise errors? 278 | - What happens if I forget to `.catch()` every promise? 279 | - How can I cancel a promise? 280 | 281 | Finally, the JS engine resolves a promise via a **microtask**. 282 | 283 | 306 | 307 | --- 308 | 309 | - macrotasks: 310 | - `setTimeout` 311 | - `setInterval` 312 | - `setImmediate` 313 | - `requestAnimationFrame` 314 | - UI rendering 315 | - microtasks: 316 | - `Promise` 317 | - `process.nextTick` 318 | - `queueMicrotask` 319 | - `MutationObserver` 320 | 321 | Read more about microtasks.[^3] 322 | 323 | 328 | 329 | --- 330 | 331 | # What are delimited continuations (DC)? 332 | 333 | - It's all about flow control 334 | - Doesn't matter if it's sync or async, DCs look the same 335 | - DC is the abstraction upon which all flow control paradigms can be built 336 | - **continuation:** The rest of code execution **reified (A)** inside a function 337 | - **delimited:** The **reified (A)** function can return a value -- which allows 338 | for composition 339 | - Two primitives -- `shift()` and `reset()` -- are all you need for complex 340 | async flow control 341 | 342 | 357 | 358 | --- 359 | 360 | # Detour: Yield delegates 361 | 362 | Typescript has basic support for generators but not for the type of constructs 363 | we are building.[^4] 364 | 365 | ```ts 366 | function* fun() { 367 | return number; 368 | } 369 | 370 | function* raw() { 371 | const value = yield fun(); // value = any 372 | } 373 | ``` 374 | 375 | 385 | 386 | --- 387 | 388 | Yield delegates `yield *` provide a way to reach into the generator and have it 389 | output the proper type. 390 | 391 | ```ts 392 | function* fun() { 393 | return number; 394 | } 395 | 396 | function* typed() { 397 | const value = yield* fun(); // value = number 398 | } 399 | ``` 400 | 401 | Now we can use some clever tricks to get the proper types out of our `yield` 402 | statements. 403 | 404 | 409 | 410 | --- 411 | 412 | # Code examples 413 | 414 | Using `continuation`[^5] 415 | 416 | 424 | 425 | --- 426 | 427 | ## Sync example 428 | 429 | The first example I would like to show is one of simple string concatenation. 430 | 431 | --- 432 | 433 | The entire flow of the `evaluate()` function is synchronous in this example and 434 | as such we can use it like a normal sync function. 435 | 436 | ```ts 437 | import { Computation, shift, evaluate } from './deps.ts'; 438 | 439 | function slot(): Computation { 440 | return shift(function* (k) { 441 | return k; 442 | }); 443 | } 444 | 445 | function dc() { 446 | return evaluate<(s: string) => (s: string) => string>(function* () { 447 | const greeting = yield* slot(); 448 | const thing = yield* slot(); 449 | return `${greeting}, ${thing}!`; 450 | }); 451 | } 452 | 453 | const tmpl = dc(); 454 | const result = tmpl('hello')('world'); 455 | console.log(result); // hello, world! 456 | ``` 457 | 458 | 467 | 468 | --- 469 | 470 | ### key points 471 | 472 | - `k` is the continuation "callback" 473 | - When you return `k` from a shift, the evaluate function returns that `k` 474 | function as its value 475 | - Inside the evaluate function `greeting` and `thing` are the values passed into 476 | it from outside the `evaluate` body function 477 | - `shift()` can be thought of as slots that eventually get filled by values 478 | passed into `evaluate()`! 479 | 480 | --- 481 | 482 | ![bg contain](./img/calc.png) 483 | 484 | 489 | 490 | --- 491 | 492 | ```ts 493 | function dc() { 494 | return (left: string) => { 495 | return (right: string) => { 496 | return `${left}, ${right}!`; 497 | }; 498 | }; 499 | } 500 | 501 | const tmpl = dc(); 502 | const result = tmpl('hello')('world'); 503 | console.log(result); // hello, world! 504 | ``` 505 | 506 | 515 | 516 | --- 517 | 518 | Continuation-passing style[^6] 519 | 520 | ```ts 521 | function cps(cont: (s: string) => void) { 522 | return (left: string) => { 523 | return (right: string) => { 524 | cont(`${left}, ${right}!`); 525 | }; 526 | }; 527 | } 528 | 529 | const tmpl = cps((result) => { 530 | console.log(result); // hello, world! 531 | }); 532 | tmpl('hello')('world'); 533 | ``` 534 | 535 | 548 | 549 | --- 550 | 551 | ## Async example 552 | 553 | In the next example, I would like to show how we can incorporate async flow 554 | control. 555 | 556 | ```ts 557 | import { evaluate, shift } from './deps.ts'; 558 | 559 | const run = evaluate<(n: number) => Promise>(function* () { 560 | const left = yield* shift(function* (k) { 561 | return k; 562 | }); 563 | 564 | const right = yield* shift(function* (resolve) { 565 | return Promise.resolve(55).then(resolve); 566 | }); 567 | 568 | return left + right; 569 | }); 570 | 571 | const result = await run(13); 572 | console.log(result); // 68 573 | ``` 574 | 575 | 579 | 580 | --- 581 | 582 | ### key points 583 | 584 | - It doesn't matter if it's sync or async, delimited continuations can handle 585 | that flow control 586 | - The `right` shift returns a promise which is what gets returned when calling 587 | `run(13)` 588 | - Since the return value is a promise, we `await` the answer 589 | - I renamed the `shift` `k` variable to `resolve` to demonstrate how similar it 590 | is to a promise "continuation." 591 | - When you use a promise, it will always be async 592 | - When you use a delimited continuation, it might be async 593 | - It doesn't matter if the flow of code execution is sync or async, delimited 594 | continuations handle them the exact same 595 | 596 | --- 597 | 598 | ## Composition example 599 | 600 | ```ts 601 | import { shift, evaluate, Computation, Continuation } from "./deps.ts"; 602 | 603 | type ShiftProp = ( 604 | res: Continuation, 605 | rej?: Continuation 606 | ) => Computation; 607 | 608 | function* add( 609 | lhs: ShiftProp, 610 | rhs: ShiftProp 611 | ): Computation { 612 | const left = yield* shift(lhs); 613 | const right = yield* shift(rhs); 614 | return left + right; 615 | } 616 | 617 | const sync = (value: number) => 618 | function* (k: Continuation) { 619 | return k(value); 620 | }; 621 | 622 | const ev = evaluate(function* () { 623 | const first = yield* add(sync(13), function* (k) { 624 | return Promise.resolve(55).then(k); 625 | }); 626 | 627 | const second = yield* add( 628 | function* (k) { 629 | setTimeout(() => k(21), 1000); 630 | }, 631 | function* (k) { 632 | k(Math.random()); 633 | } 634 | ); 635 | 636 | const result = yield* add(sync(first), sync(second)); 637 | console.log(result); 638 | return result; 639 | }); 640 | 641 | console.log(ev); 642 | ``` 643 | 644 | 660 | 661 | --- 662 | 663 | # Counter example 664 | 665 | ```ts 666 | interface Count { 667 | value: number; 668 | increment(): Count; 669 | } 670 | 671 | function useCounter() { 672 | return reset(function* () { 673 | for (let i = 0; ; i++) { 674 | yield* shift(function* (k) { 675 | return { value: i, increment: k }; 676 | }); 677 | } 678 | }); 679 | } 680 | 681 | let start = evaluate(useCounter); 682 | 683 | let once = start.increment(); 684 | let twice = once.increment(); 685 | let thrice = twice.increment(); 686 | 687 | console.dir([once.value, twice.value, thrice.value]); // [1, 2, 3] 688 | ``` 689 | 690 | 709 | 710 | --- 711 | 712 | ![bg left](./img/db_vader.jpg) 713 | 714 | # wat 715 | 716 | --- 717 | 718 | ![bg left](./img/arrow.jpg) 719 | 720 | # wat 721 | 722 | --- 723 | 724 | I'm still very confused by the coding paradigm. Everytime I look at this code I 725 | get a headache. 726 | 727 | Most end-developers aren't going to be using delimited continuations directly. 728 | 729 | Rather, this tool will allow library developers to build on top of it. 730 | 731 | --- 732 | 733 | ## effection v3[^7] 734 | 735 | `effection` takes `continuation` and builds a task tree. All tasks are cleaned 736 | up automatically via a set of cancellation strategies: 737 | 738 | - tasks spawn other tasks 739 | - tasks can be `halt()`ed 740 | - All descendants are `halt()`ed 741 | - All ancestors are `halt()`ed 742 | 743 | 746 | 747 | --- 748 | 749 | `effection` also has higher level compositions of `shift()` and `reset()` which 750 | grants us a flourish of functionality. For example: 751 | 752 | - `suspend()`: permenantly suspend a generator function at a `yield` point. 753 | - `action()`: which is a wrapper for `shift()` with proper cleanup of the task 754 | - `spawn()`: creates a sub-task 755 | - `createChannel()`: kind of like an event emitter but using DC 756 | - `sleep(n)`: temporarily suspend generator function for for (n) milliseconds 757 | 758 | --- 759 | 760 | ![bg left](./img/confused.jpg) 761 | 762 | # I still don't get it 763 | 764 | 776 | 777 | --- 778 | 779 | Building on top of `effection` we now have a set of middle-level primitives that 780 | allow us to build any flow control paradigm we want. 781 | 782 | 796 | 797 | --- 798 | 799 | Jake Archibald recently wrote an interesting article about unhandled 800 | rejections.[^8] 801 | 802 | In it he talks about the use case of wanting to fetch book chapters in parallel 803 | but process them in sequence: 804 | 805 | ```ts 806 | async function showChapters(chapterURLs) { 807 | const chapterPromises = chapterURLs.map(async (url) => { 808 | const response = await fetch(url); 809 | return response.json(); 810 | }); 811 | 812 | for await (const chapterData of chapterPromises) { 813 | appendChapter(chapterData); 814 | } 815 | } 816 | ``` 817 | 818 | 826 | 827 | --- 828 | 829 | He goes on to describe the the potential for bugs because of 830 | `unhandled rejections`. Below is the "final" solution. 831 | 832 | ```diff 833 | async function showChapters(chapterURLs) { 834 | const chapterPromises = chapterURLs.map(async (url) => { 835 | const response = await fetch(url); 836 | return response.json(); 837 | }); 838 | 839 | + for (const promise of chapterPromises) promise.catch(() => {}); 840 | 841 | for await (const chapterData of chapterPromises) { 842 | appendChapter(chapterData); 843 | } 844 | } 845 | ``` 846 | 847 | 857 | 858 | --- 859 | 860 | # `starfx`[^9] 861 | 862 | ![bg right:66%](./img/starfx.png) 863 | 864 | 872 | 873 | --- 874 | 875 | Using `starfx`[^9] we could do something like this: 876 | 877 | ```ts 878 | import { request, json, parallel, forEach } from 'starfx'; 879 | 880 | function* showChapters(chapterURLs: string[]) { 881 | const reqs = chapterURLs.map(function (url) { 882 | return function* () { 883 | const response = yield* request(url); 884 | return yield* json(response); 885 | }; 886 | }); 887 | 888 | const chapters = yield* parallel(reqs); 889 | 890 | yield* forEach(chapters.sequence, function* (chapter) { 891 | if (chapter.ok) { 892 | appendChapter(chapter.value); 893 | } else { 894 | console.error(chapter.error); 895 | } 896 | }); 897 | } 898 | ``` 899 | 900 | 919 | 920 | --- 921 | 922 | No uncaught exceptions, code is just as simple to understand. 923 | 924 | Further, we automatically pass an `AbortController.signal` to all http requests 925 | because of the `request` fx we wrote. 926 | 927 | When a task is `halt()`ed or crashes, we trigger the signal to abort the `fetch` 928 | call. 929 | 930 | Automatic cleanup! 931 | 932 | 941 | 942 | --- 943 | 944 | # We are just getting started 945 | 946 | ![bg right](./img/tree.png) 947 | 948 | Things we plan on building: 949 | 950 | - Inspector / debugger for `effection` 951 | - Side-effect system for `redux` (ala `redux-saga`) 952 | - Query and cache management (ala `react-query`) 953 | - Web server (ala `express`) 954 | - View library (ala `react`) 955 | 956 | 965 | 966 | --- 967 | 968 | ![bg](./img/yellow-to-lavender.svg) 969 | 970 | ![aptible logo](./img/aptible.svg) [^10] 971 | 972 | The most successful PaaS you didn't know existed. 973 | 974 | https://aptible.com 975 | 976 | 985 | 986 | --- 987 | 988 | # The Frontside[^11] 989 | 990 | https://frontside.com 991 | 992 | https://discord.gg/frontside 993 | 994 | 998 | 999 | --- 1000 | 1001 | # fin 1002 | 1003 | [^1]: https://blog.container-solutions.com/is-it-imperative-to-be-declarative 1004 | [^2]: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function 1005 | [^3]: https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules 1006 | [^4]: https://github.com/microsoft/TypeScript/issues/32523 1007 | [^5]: https://github.com/thefrontside/continuation 1008 | [^6]: https://en.wikipedia.org/wiki/Continuation-passing_style 1009 | [^7]: https://github.com/thefrontside/effection 1010 | [^8]: https://jakearchibald.com/2023/unhandled-rejections/ 1011 | [^9]: https://github.com/neurosnap/starfx 1012 | [^10]: https://aptible.com 1013 | [^11]: https://frontside.com 1014 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neurosnap/deck-continuations/fbbb912092adc557ad4e61f3b8c15d0956037cf1/deno.json -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2", 3 | "remote": { 4 | "https://deno.land/x/continuation@0.1.5/mod.ts": "690def2735046367b3e1b4bc6e51b5912f2ed09c41c7df7a55c060f23720ad33" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ex/async.ts: -------------------------------------------------------------------------------- 1 | import { evaluate, shift } from "./deps.ts"; 2 | 3 | const run = evaluate<(n: number) => Promise>(function* () { 4 | const left = yield* shift(function* (k) { 5 | return k; 6 | }); 7 | 8 | const right = yield* shift(function* (resolve) { 9 | return Promise.resolve(55).then(resolve); 10 | }); 11 | 12 | return left + right; 13 | }); 14 | 15 | const result = await run(13); 16 | console.log(result); // 68 17 | -------------------------------------------------------------------------------- /ex/callback.ts: -------------------------------------------------------------------------------- 1 | function plan() { 2 | const left = "hello"; 3 | const right = "world"; 4 | return `${left}, ${right}!`; 5 | } 6 | 7 | function dc() { 8 | // reify the continuation of this function in a function 9 | return (left: string) => { 10 | // reify the continuation of this function in a function 11 | return (right: string) => { 12 | // return the result 13 | return `${left}, ${right}!`; 14 | }; 15 | }; 16 | } 17 | 18 | const tmpl = dc(); 19 | const result = tmpl("hello")("world"); 20 | console.log(result); 21 | 22 | function cps(cont: (s: string) => void) { 23 | return (left: string) => { 24 | return (right: string) => { 25 | cont(`${left}, ${right}!`); 26 | }; 27 | }; 28 | } 29 | 30 | const tmplCps = cps((result) => { 31 | console.log(result); 32 | }); 33 | tmplCps('hello')('world'); 34 | -------------------------------------------------------------------------------- /ex/compose.ts: -------------------------------------------------------------------------------- 1 | import { shift, evaluate, Computation, Continuation } from "./deps.ts"; 2 | 3 | type ShiftProp = ( 4 | res: Continuation, 5 | rej?: Continuation 6 | ) => Computation; 7 | 8 | function* add( 9 | lhs: ShiftProp, 10 | rhs: ShiftProp 11 | ): Computation { 12 | const left = yield* shift(lhs); 13 | const right = yield* shift(rhs); 14 | return left + right; 15 | } 16 | 17 | const sync = (value: number) => 18 | function* (k: Continuation) { 19 | return k(value); 20 | }; 21 | 22 | function* async(): Computation<{ 23 | resolve: (r: T) => void; 24 | reject: (e: Error) => void; 25 | }> { 26 | return yield* shift(function* (k) { 27 | const promise = new Promise((resolve, reject) => { 28 | k({ resolve, reject }); 29 | }); 30 | 31 | return promise; 32 | }); 33 | } 34 | 35 | const ev = evaluate>(function* () { 36 | const { resolve } = yield* async(); 37 | 38 | const first = yield* add(sync(13), function* (k) { 39 | return Promise.resolve(55).then(k); 40 | }); 41 | 42 | const second = yield* add( 43 | function* (k) { 44 | setTimeout(() => k(21), 1000); 45 | }, 46 | function* (k) { 47 | k(Math.random()); 48 | } 49 | ); 50 | 51 | const result = yield* add(sync(first), sync(second)); 52 | resolve(result); 53 | }); 54 | 55 | const result = await ev; 56 | console.log(result); 57 | -------------------------------------------------------------------------------- /ex/counter.ts: -------------------------------------------------------------------------------- 1 | import { reset, shift, evaluate } from "./deps.ts"; 2 | 3 | interface Count { 4 | value: number; 5 | increment(): Count; 6 | } 7 | 8 | function useCounter() { 9 | return reset(function* () { 10 | for (let i = 0; ; i++) { 11 | yield* shift(function* (k) { 12 | return { value: i, increment: k }; 13 | }); 14 | } 15 | }); 16 | } 17 | 18 | const start = evaluate(useCounter); 19 | 20 | const once = start.increment(); 21 | const twice = once.increment(); 22 | const thrice = twice.increment(); 23 | 24 | console.dir([once.value, twice.value, thrice.value]); // [1, 2, 3] 25 | -------------------------------------------------------------------------------- /ex/create-template.ts: -------------------------------------------------------------------------------- 1 | import { Computation, shift, evaluate } from "./deps.ts"; 2 | 3 | function slot(): Computation { 4 | return shift(function* (k) { 5 | return k; 6 | }); 7 | } 8 | 9 | function dc() { 10 | return evaluate<(s: string) => (s: string) => string>(function* () { 11 | const greeting = yield* slot(); 12 | const thing = yield* slot(); 13 | return `${greeting}, ${thing}!`; 14 | }); 15 | } 16 | 17 | const tmpl = dc(); 18 | const result = tmpl("hello")("world"); 19 | console.log(result); 20 | -------------------------------------------------------------------------------- /ex/deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | shift, 3 | evaluate, 4 | reset, 5 | } from "https://deno.land/x/continuation@0.1.5/mod.ts"; 6 | 7 | export type { Computation, Continuation } from "https://deno.land/x/continuation@0.1.5/mod.ts"; 8 | -------------------------------------------------------------------------------- /ex/starfx.ts: -------------------------------------------------------------------------------- 1 | import { request, json, parallel, forEach } from 'starfx'; 2 | 3 | function* showChapters(chapterURLs: string[]) { 4 | const reqs = chapterURLs.map(function (url) { 5 | return function* () { 6 | const response = yield* request(url); 7 | return yield* json(response); 8 | }; 9 | }); 10 | 11 | const chapters = yield* parallel(reqs); 12 | 13 | yield* forEach(chapters.sequence, function* (chapter) { 14 | if (chapter.ok) { 15 | appendChapter(chapter.value); 16 | } else { 17 | console.error(chapter.error); 18 | } 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /ex/yield-delegate.ts: -------------------------------------------------------------------------------- 1 | import { evaluate } from "./deps.ts"; 2 | 3 | function* fun() { 4 | return 1; 5 | } 6 | 7 | function* raw(): Generator { 8 | const val = yield fun(); 9 | return val; 10 | } 11 | 12 | function* typed() { 13 | const val = yield* fun(); 14 | return val; 15 | } 16 | 17 | console.log(evaluate(typed)); 18 | -------------------------------------------------------------------------------- /img/aptible.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /img/arrow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neurosnap/deck-continuations/fbbb912092adc557ad4e61f3b8c15d0956037cf1/img/arrow.jpg -------------------------------------------------------------------------------- /img/calc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neurosnap/deck-continuations/fbbb912092adc557ad4e61f3b8c15d0956037cf1/img/calc.png -------------------------------------------------------------------------------- /img/callback-hell.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neurosnap/deck-continuations/fbbb912092adc557ad4e61f3b8c15d0956037cf1/img/callback-hell.jpg -------------------------------------------------------------------------------- /img/confused.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neurosnap/deck-continuations/fbbb912092adc557ad4e61f3b8c15d0956037cf1/img/confused.jpg -------------------------------------------------------------------------------- /img/db_vader.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neurosnap/deck-continuations/fbbb912092adc557ad4e61f3b8c15d0956037cf1/img/db_vader.jpg -------------------------------------------------------------------------------- /img/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neurosnap/deck-continuations/fbbb912092adc557ad4e61f3b8c15d0956037cf1/img/qrcode.png -------------------------------------------------------------------------------- /img/starfx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neurosnap/deck-continuations/fbbb912092adc557ad4e61f3b8c15d0956037cf1/img/starfx.png -------------------------------------------------------------------------------- /img/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neurosnap/deck-continuations/fbbb912092adc557ad4e61f3b8c15d0956037cf1/img/tree.png -------------------------------------------------------------------------------- /img/yellow-to-lavender.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | --------------------------------------------------------------------------------