Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Dominic Elm ABOUT ME

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

• 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

Slide 6

Slide 6 text

Read the article here

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

WHY IS IT IMPORTANT? Modern web apps are highly interactive and asynchonous, e.g. DOM Events HTTP Requests Web Sockets Animations Server Sent Events

Slide 9

Slide 9 text

WHY IS IT IMPORTANT? Managing async is hard

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

WHY IS IT IMPORTANT? How do we deal with such interactive experience and asynchrony? Introducing RxJS

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Let’s learn how to think reactively

Slide 17

Slide 17 text

THE GAME

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Modifying state that is visible to other functions = Side Effect

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Functional programming tries to avoid side effects

Slide 25

Slide 25 text

Side effects increase the complexity of a function

Slide 26

Slide 26 text

Side effects are not evil and sometimes not avoidable

Slide 27

Slide 27 text

Have clear intentions for introducing side effects

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

1. Breaking up a problem into smaller pieces 2. Identifying inital triggers 3. Composing final event stream APPROACH

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

STEERING MECHANISM

Slide 34

Slide 34 text

STEERING MECHANISM Keydown Events = Initial Trigger

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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 }

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

PLAYER SCORE snake$ apples$ length$: BehaviorSubject 5 6 7 snakeLength$

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

No content

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

SNAKE What is the inital trigger for the snake?

Slide 59

Slide 59 text

SNAKE What is the inital trigger for the snake?

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

Let’s compose our streams to create the scene$

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

Let’s render the scene$

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

DEMO

Slide 77

Slide 77 text

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