Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Taming snakes with reactive streams

7764379521726735c164889159c8f387?s=47 Dominic Elm
December 09, 2017

Taming snakes with reactive streams

The web moves quickly and we all know it. Today, Reactive Programming is one of the hottest topics in web development and with frameworks like Angular or React, it has become much more popular especially in the modern JavaScript world. There’s been a massive move in the community from imperative programming paradigms to functional reactive paradigms. Yet, many developers struggle with it and are often overwhelmed by its complexity (large API), fundamental shift in mindset (from imperative to declarative) and the multitude of concepts.

The goal of this talk is to give you an idea how to approach a problem in a functional reactive way by building a classic video game that we all know and love - Snake. Games are fun, but at the same time complex systems that keep a lot of external state, e.g. scores, timers, or player coordinates. For our version, we’ll make heavy use of Observables and use several different operators to completely avoid external state.

Links:
https://github.com/thoughtram/reactive-snake
https://blog.thoughtram.io/rxjs/2017/08/24/taming-snakes-with-reactive-streams.html

7764379521726735c164889159c8f387?s=128

Dominic Elm

December 09, 2017
Tweet

Transcript

  1. Taming snakes with reactive streams Trainer @thoughtram twitter.com/elmd_

  2. Dominic Elm ABOUT ME

  3. Blogging blog.thoughtram.io Dominic Elm ABOUT ME Trainer @thoughtram @elmd_ =

  4. Blogging blog.thoughtram.io Dominic Elm ABOUT ME Trainer @thoughtram @elmd_ =

  5. • What is reactive programming? • Why it is important?

    • How to think reactively • Learn common operators and how to accumulate state • Implementing Snake with RxJS WHAT TO EXPECT
  6. Read the article here

  7. WHAT IS REACTIVE? Reactive programming is programming with asynchronous data

    streams. A stream is a sequence of ongoing events ordered in time that offer some hooks with which to observe it. Source: Andre Staltz
  8. WHY IS IT IMPORTANT? Modern web apps are highly interactive

    and asynchonous, e.g. DOM Events HTTP Requests Web Sockets Animations Server Sent Events
  9. WHY IS IT IMPORTANT? Managing async is hard

  10. WHY IS IT IMPORTANT? How do we deal with such

    interactive experience and asynchrony?
  11. WHY IS IT IMPORTANT? How do we deal with such

    interactive experience and asynchrony? Introducing RxJS
  12. RxJS brings the concept of functional reactive programming to the

    web Makes it easy to deal with all sorts of synchronous and asynchronous data streams Introduces Observable type to represent data streams
  13. map( => ) Producer (source stream) Operators (pipeline) filter( )

    Consumer myStream$.subscribe(console.log) let myStream$
  14. Observables are… Composable Proxies for values Single or Multiple Values

    First-class objects Immutable Lazy Cancelable
  15. Observables are… Composable Proxies for values Single or Multiple Values

    First-class objects Immutable Lazy Cancelable
  16. Let’s learn how to think reactively

  17. THE GAME

  18. OUR GOALS • Re-implement Snake • Only use HTML5, JavaScript

    and RxJS • Transform programatic event-loop (imperative) into a reactive-event driven app • NO external state outside the observable pipeline
  19. Modifying state that is visible to other functions = Side

    Effect
  20. map( => counter++ ) filter( ) Producer (source stream) Result

    (result stream) Operators (pipeline) let counter = 0; 0 1 2
  21. map( => counter++ ) filter( ) Producer (source stream) Result

    (result stream) Operators (pipeline) let counter = 0; 0 1 2 External State
  22. map( => counter++ ) filter( ) Producer (source stream) Result

    (result stream) Operators (pipeline) let counter = 0; 0 1 2 Side Effect External State
  23. map( => counter++ ) filter( ) Producer (source stream) Result

    (result stream) Operators (pipeline) let counter = 0; 0 1 2 Side Effect External State
  24. Functional programming tries to avoid side effects

  25. Side effects increase the complexity of a function

  26. Side effects are not evil and sometimes not avoidable

  27. Have clear intentions for introducing side effects

  28. scan((acc, ) => acc++, 0) filter( ) Producer (source stream)

    Result (result stream) Operators (pipeline) 0 1 2
  29. scan((acc, ) => acc++, 0) filter( ) Producer (source stream)

    Result (result stream) Operators (pipeline) 0 1 2
  30. 1. Breaking up a problem into smaller pieces 2. Identifying

    inital triggers 3. Composing final event stream APPROACH
  31. BUILDING BLOCKS • Steering mechanism • Player’s score • Snake

    • Apples
  32. BUILDING BLOCKS • Steering mechanism • Player’s score • Snake

    • Apples
  33. STEERING MECHANISM

  34. STEERING MECHANISM Keydown Events = Initial Trigger

  35. None
  36. let keydown$ = fromEvent(document, 'keydown'); let direction$ = keydown$.pipe( map((event:

    KeyboardEvent) => DIRECTIONS[event.keyCode]), filter(direction => !!direction), startWith(INITIAL_DIRECTION), scan(nextDirection), distinctUntilChanged() );
  37. let keydown$ = fromEvent(document, 'keydown'); let direction$ = keydown$.pipe( map((event:

    KeyboardEvent) => DIRECTIONS[event.keyCode]), filter(direction => !!direction), startWith(INITIAL_DIRECTION), scan(nextDirection), distinctUntilChanged() ); DIRECTIONS is a key/value map for all possible directions, e.g. DIRECTIONS[39] = { x: 1, y: 0 }
  38. let keydown$ = fromEvent(document, 'keydown'); let direction$ = keydown$.pipe( map((event:

    KeyboardEvent) => DIRECTIONS[event.keyCode]), filter(direction => !!direction), startWith(INITIAL_DIRECTION), scan(nextDirection), distinctUntilChanged() );
  39. let keydown$ = fromEvent(document, 'keydown'); let direction$ = keydown$.pipe( map((event:

    KeyboardEvent) => DIRECTIONS[event.keyCode]), filter(direction => !!direction), startWith(INITIAL_DIRECTION), scan(nextDirection), distinctUntilChanged() );
  40. let keydown$ = fromEvent(document, 'keydown'); let direction$ = keydown$.pipe( map((event:

    KeyboardEvent) => DIRECTIONS[event.keyCode]), filter(direction => !!direction), startWith(INITIAL_DIRECTION), scan(nextDirection), distinctUntilChanged() );
  41. let keydown$ = fromEvent(document, 'keydown'); let direction$ = keydown$.pipe( map((event:

    KeyboardEvent) => DIRECTIONS[event.keyCode]), filter(direction => !!direction), startWith(INITIAL_DIRECTION), scan(nextDirection), distinctUntilChanged() );
  42. BUILDING BLOCKS • Steering mechanism • Player’s score • Snake

    • Apples
  43. PLAYER SCORE snake$ apples$ [{…}, {…}, {…}] [{…}, {…}, {…}]

  44. PLAYER SCORE Input snake$ apples$ [{…}, {…}, {…}] [{…}, {…},

    {…}]
  45. PLAYER SCORE Input snake$ apples$ [{…}, {…}, {…}] [{…}, {…},

    {…}] Input
  46. PLAYER SCORE Input snake$ apples$ [{…}, {…}, {…}] [{…}, {…},

    {…}] Input ⚡ Circular Dependency
  47. PLAYER SCORE Input snake$ apples$ [{…}, {…}, {…}] [{…}, {…},

    {…}] Input ⚡ Circular Dependency
  48. PLAYER SCORE snake$ apples$ length$: BehaviorSubject 5 6 7 snakeLength$

  49. PLAYER SCORE snake$ apples$ Propagate collision length$: BehaviorSubject 5 6

    7 snakeLength$
  50. PLAYER SCORE snake$ apples$ Propagate collision length$: BehaviorSubject 5 6

    7 snakeLength$ Input
  51. PLAYER SCORE snake$ apples$ Propagate collision length$: BehaviorSubject Input 5

    6 7 snakeLength$ Input
  52. PLAYER SCORE snake$ apples$ Propagate collision length$: BehaviorSubject Input 5

    6 7 snakeLength$ Input Input
  53. None
  54. let length$ = new BehaviorSubject<number>(SNAKE_LENGTH); let snakeLength$ = length$.pipe( scan((step,

    snakeLength) => snakeLength + step), share() ); let score$ = snakeLength$.pipe( startWith(0), scan((score, _) => score + POINTS_PER_APPLE), );
  55. let length$ = new BehaviorSubject<number>(SNAKE_LENGTH); let snakeLength$ = length$.pipe( scan((snakeLength,

    _) => snakeLength + 1), share() ); let score$ = snakeLength$.pipe( startWith(0), scan((score, _) => score + POINTS_PER_APPLE), );
  56. let length$ = new BehaviorSubject<number>(SNAKE_LENGTH); let snakeLength$ = length$.pipe( scan((snakeLength,

    _) => snakeLength + 1), share() ); let score$ = snakeLength$.pipe( startWith(0), scan((score, _) => score + POINTS_PER_APPLE), );
  57. BUILDING BLOCKS • Steering mechanism • Player’s score • Snake

    • Apples
  58. SNAKE What is the inital trigger for the snake?

  59. SNAKE What is the inital trigger for the snake?

  60. None
  61. let ticks$ = interval(SPEED); let snake$: Observable<Array<Point2D>> = ticks$.pipe( withLatestFrom(direction$,

    snakeLength$, (_, direction, snakeLength) => [direction, snakeLength]), scan(move, generateSnake()), share() );
  62. let ticks$ = interval(SPEED); let snake$: Observable<Array<Point2D>> = ticks$.pipe( withLatestFrom(direction$,

    snakeLength$, (_, direction, snakeLength) => [direction, snakeLength]), scan(move, generateSnake()), share() );
  63. let ticks$ = interval(SPEED); let snake$: Observable<Array<Point2D>> = ticks$.pipe( withLatestFrom(direction$,

    snakeLength$, (_, direction, snakeLength) => [direction, snakeLength]), scan(move, generateSnake()), share() );
  64. BUILDING BLOCKS • Steering mechanism • Player’s score • Snake

    • Apples
  65. APPLES When do we need to update the list of

    apples?
  66. APPLES When do we need to update the list of

    apples?
  67. let apples$ = snake$.pipe( scan(eat, generateApples()), distinctUntilChanged(), share() ); let

    appleEaten$ = apples$.pipe( skip(1), tap(() => length$.next(1)) ).subscribe();
  68. let apples$ = snake$.pipe( scan(eat, generateApples()), distinctUntilChanged(), share() ); let

    appleEaten$ = apples$.pipe( skip(1), tap(() => length$.next(1)) ).subscribe();
  69. Let’s compose our streams to create the scene$

  70. None
  71. let scene$: Observable<Scene> = combineLatest( snake$, apples$, score$, (snake, apples,

    score) => ({ snake, apples, score }) );
  72. Let’s render the scene$

  73. let game$ = interval(1000 / FPS, animationFrame).pipe( withLatestFrom(scene$, (_, scene)

    => scene), takeWhile(scene => !isGameOver(scene)) ).subscribe({ next: (scene) => renderScene(ctx, scene), complete: () => renderGameOver(ctx) });
  74. let game$ = interval(1000 / FPS, animationFrame).pipe( withLatestFrom(scene$, (_, scene)

    => scene), takeWhile(scene => !isGameOver(scene)) ).subscribe({ next: (scene) => renderScene(ctx, scene), complete: () => renderGameOver(ctx) });
  75. let game$ = interval(1000 / FPS, animationFrame).pipe( withLatestFrom(scene$, (_, scene)

    => scene), takeWhile(scene => !isGameOver(scene)) ).subscribe({ next: (scene) => renderScene(ctx, scene), complete: () => renderGameOver(ctx) });
  76. DEMO

  77. THANK YOU! Trainer @thoughtram twitter.com/elmd_ blog.thoughtram.io machinelabs.ai