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

Taming snakes with reactive streams

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

Dominic Elm

December 09, 2017
Tweet

More Decks by Dominic Elm

Other Decks in Programming

Transcript

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

    View full-size slide

  2. Dominic Elm
    ABOUT ME

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  6. Read the article here

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  9. WHY IS IT IMPORTANT?
    Managing async is hard

    View full-size slide

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

    experience and asynchrony?

    View full-size slide

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

    experience and asynchrony?
    Introducing RxJS

    View full-size slide

  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

    View full-size slide

  13. map( => )
    Producer
    (source stream)
    Operators
    (pipeline)
    filter( )
    Consumer myStream$.subscribe(console.log)
    let myStream$

    View full-size slide

  14. Observables are…
    Composable

    Proxies for values

    Single or Multiple Values

    First-class objects

    Immutable

    Lazy

    Cancelable

    View full-size slide

  15. Observables are…
    Composable

    Proxies for values

    Single or Multiple Values

    First-class objects

    Immutable

    Lazy

    Cancelable

    View full-size slide

  16. Let’s learn how to think
    reactively

    View full-size slide

  17. 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

    View full-size slide

  18. Modifying state that is visible to
    other functions

    =

    Side Effect

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  23. Functional programming tries to
    avoid side effects

    View full-size slide

  24. Side effects increase the
    complexity of a function

    View full-size slide

  25. Side effects are not evil and
    sometimes not avoidable

    View full-size slide

  26. Have clear intentions for
    introducing side effects

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  29. 1. Breaking up a problem into smaller pieces
    2. Identifying inital triggers

    3. Composing final event stream
    APPROACH

    View full-size slide

  30. BUILDING BLOCKS
    • Steering mechanism

    • Player’s score

    • Snake

    • Apples

    View full-size slide

  31. BUILDING BLOCKS
    • Steering mechanism

    • Player’s score

    • Snake

    • Apples

    View full-size slide

  32. STEERING MECHANISM

    View full-size slide

  33. STEERING MECHANISM
    Keydown Events = Initial Trigger

    View full-size slide

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

    View full-size slide

  35. 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 }

    View full-size slide

  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()
    );

    View full-size slide

  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()
    );

    View full-size slide

  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()
    );

    View full-size slide

  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()
    );

    View full-size slide

  40. BUILDING BLOCKS
    • Steering mechanism

    • Player’s score

    • Snake

    • Apples

    View full-size slide

  41. PLAYER SCORE


    snake$ apples$
    [{…}, {…}, {…}] [{…}, {…}, {…}]

    View full-size slide

  42. PLAYER SCORE

    Input
    snake$ apples$
    [{…}, {…}, {…}] [{…}, {…}, {…}]

    View full-size slide

  43. PLAYER SCORE

    Input
    snake$ apples$
    [{…}, {…}, {…}] [{…}, {…}, {…}]
    Input

    View full-size slide

  44. PLAYER SCORE

    Input
    snake$ apples$
    [{…}, {…}, {…}] [{…}, {…}, {…}]
    Input
    ⚡ Circular Dependency

    View full-size slide

  45. PLAYER SCORE

    Input
    snake$ apples$
    [{…}, {…}, {…}] [{…}, {…}, {…}]
    Input
    ⚡ Circular Dependency

    View full-size slide

  46. PLAYER SCORE


    snake$ apples$

    length$: BehaviorSubject
    5 6 7
    snakeLength$

    View full-size slide

  47. PLAYER SCORE


    snake$ apples$

    Propagate collision
    length$: BehaviorSubject
    5 6 7
    snakeLength$

    View full-size slide

  48. PLAYER SCORE


    snake$ apples$

    Propagate collision
    length$: BehaviorSubject
    5 6 7
    snakeLength$
    Input

    View full-size slide

  49. PLAYER SCORE


    snake$ apples$

    Propagate collision
    length$: BehaviorSubject
    Input
    5 6 7
    snakeLength$
    Input

    View full-size slide

  50. PLAYER SCORE


    snake$ apples$

    Propagate collision
    length$: BehaviorSubject
    Input
    5 6 7
    snakeLength$
    Input
    Input

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  54. BUILDING BLOCKS
    • Steering mechanism

    • Player’s score

    • Snake

    • Apples

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  60. BUILDING BLOCKS
    • Steering mechanism

    • Player’s score

    • Snake

    • Apples

    View full-size slide

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

    View full-size slide

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



    View full-size slide

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

    View full-size slide

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

    View full-size slide

  65. Let’s compose our streams to
    create the scene$

    View full-size slide

  66. let scene$: Observable = combineLatest(
    snake$,
    apples$,
    score$,
    (snake, apples, score) => ({ snake, apples, score })
    );

    View full-size slide

  67. Let’s render the scene$

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide