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. • 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
  2. 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
  3. WHY IS IT IMPORTANT? Modern web apps are highly interactive

    and asynchonous, e.g. DOM Events HTTP Requests Web Sockets Animations Server Sent Events
  4. WHY IS IT IMPORTANT? How do we deal with such

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

    interactive experience and asynchrony? Introducing RxJS
  6. 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
  7. map( => ) Producer (source stream) Operators (pipeline) filter( )

    Consumer myStream$.subscribe(console.log) let myStream$
  8. 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
  9. map( => counter++ ) filter( ) Producer (source stream) Result

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

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

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

    (result stream) Operators (pipeline) let counter = 0; 0 1 2 Side Effect External State
  13. scan((acc, ) => acc++, 0) filter( ) Producer (source stream)

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

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

    inital triggers 3. Composing final event stream APPROACH
  16. let keydown$ = fromEvent(document, 'keydown'); let direction$ = keydown$.pipe( map((event:

    KeyboardEvent) => DIRECTIONS[event.keyCode]), filter(direction => !!direction), startWith(INITIAL_DIRECTION), scan(nextDirection), distinctUntilChanged() );
  17. 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 }
  18. let keydown$ = fromEvent(document, 'keydown'); let direction$ = keydown$.pipe( map((event:

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

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

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

    KeyboardEvent) => DIRECTIONS[event.keyCode]), filter(direction => !!direction), startWith(INITIAL_DIRECTION), scan(nextDirection), distinctUntilChanged() );
  22. 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), );
  23. 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), );
  24. 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), );
  25. let ticks$ = interval(SPEED); let snake$: Observable<Array<Point2D>> = ticks$.pipe( withLatestFrom(direction$,

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

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

    snakeLength$, (_, direction, snakeLength) => [direction, snakeLength]), scan(move, generateSnake()), share() );
  28. let apples$ = snake$.pipe( scan(eat, generateApples()), distinctUntilChanged(), share() ); let

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

    appleEaten$ = apples$.pipe( skip(1), tap(() => length$.next(1)) ).subscribe();
  30. let game$ = interval(1000 / FPS, animationFrame).pipe( withLatestFrom(scene$, (_, scene)

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

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

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