Skip to content

Commit 1c32abc

Browse files
authored
Merge pull request #119 from rhys-newbury/master
merge vs switch vs concat
2 parents f2d0742 + c0eb93b commit 1c32abc

File tree

6 files changed

+57
-23
lines changed

6 files changed

+57
-23
lines changed

_chapters/frpanimated.md

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,6 @@ This page will support the workshop solutions with a worked example of how we ca
1010

1111
## Animation Generation
1212

13-
rxviz.com was used to create the visualizations below.
14-
15-
Here is a neat function allowing us to add a delay to an Observable stream for visualization purposes.
16-
17-
```typescript
18-
const addDelay =
19-
<T>(time : number) =>
20-
(obs : Observable<T>) => // zipping the interval stream with the given observable
21-
zip(interval(time), obs) // so that they are emitted at a controlled rate.
22-
.pipe(map(([[_,e]) => e)) // Just emit the elements from the original stream (ignore the output of interval)
23-
```
24-
2513
Consider, the definitions for ranks, suits and card, as per the workshop:
2614

2715
```typescript

_chapters/functionalreactiveprogramming.md

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ of(1,2,3,4)
4646
> 3
4747
> 4
4848
49-
![Mouse drag geometry](/assets/images/chapterImages/functionalreactiveprogramming/of1234.gif)
49+
![Using Of](/assets/images/chapterImages/functionalreactiveprogramming/of1234.gif)
5050

5151
The requirement to invoke `subscribe` before anything is produced by the Observable is conceptually similar to the [lazy sequence](lazyevaluation), where nothing happened until we started calling `next`. But there is also a difference.
5252
You could think of our lazy sequences as being “pull-based” data structures, because we had to “pull” the values out one at a time by calling the `next` function as many times as we wanted elements of the list. Observables are a bit different. They are used to handle “streams” of things, such as asynchronous UI (e.g. mouse clicks on an element of a web page) or communication events (e.g. responses from a web service). These things are asynchronous in the sense that we do not know when they will occur.
@@ -71,9 +71,19 @@ range(10)
7171
7272
The three animations represent the creation (`range`) and the two transformations (`filter` and `map`), respectively.
7373

74-
![Mouse drag geometry](/assets/images/chapterImages/functionalreactiveprogramming/even.gif)
74+
![Even Numbers](/assets/images/chapterImages/functionalreactiveprogramming/even.gif)
7575

76-
To solve the first Project Euler problem using RxJS, we generate a sequence of numbers from 0 to 999 with `range(1000)`. We then use the `filter` operator to select numbers divisible by 3 or 5. The `scan` operator, akin to reduce, accumulates the sum of these filtered numbers over time, and the `last` operator emits only the final accumulated sum. Finally, we subscribe to the observable and log the result to the console. Here’s the complete code:
76+
We can relate this to similar operations on arrays which we have seen before:
77+
78+
```javascript
79+
const range = n => Array(n).fill().map((_, i) => i)
80+
range(10)
81+
.filter(isEven)
82+
.map(square)
83+
.forEach(console.log)
84+
```
85+
86+
To solve the first Project Euler problem using RxJS, we generate a sequence of numbers from 0 to 999 with `range(1000)`. We then use the `filter` operator to select numbers divisible by 3 or 5. We then use he `scan` operator, akin to reduce, accumulates the sum of these filtered numbers over time, and the `last` operator emits only the final accumulated sum. Finally, we subscribe to the observable and log the result to the console. Here’s the complete code:
7787

7888
```javascript
7989
range(1000)
@@ -90,7 +100,7 @@ In the developer console, only one number will be printed:
90100
91101
We can see the values changes as they move further and further down the stream. The four animations represent the creation (`range`) and the three transformations (`filter`, `scan` and `last`), respectively. The `last` animation is empty, since we only emit the *last* value, which will be off screen.
92102

93-
![Mouse drag geometry](/assets/images/chapterImages/functionalreactiveprogramming/euler.gif)
103+
![Euler Example](/assets/images/chapterImages/functionalreactiveprogramming/euler.gif)
94104

95105
Scan is very much like the `reduce` function on Array in that it applies an accumulator function to the elements coming through the Observable, except instead of just outputting a single value (as `reduce` does), it emits a stream of the running accumulation (in this case, the sum so far). Thus, we use the `last` function to produce an Observable with just the final value.
96106

@@ -109,7 +119,7 @@ zip(columns,rows)
109119
> ["B",1]
110120
> ["C",2]
111121
112-
![Mouse drag geometry](/assets/images/chapterImages/functionalreactiveprogramming/zip1.gif)
122+
![Zip Example](/assets/images/chapterImages/functionalreactiveprogramming/zip1.gif)
113123

114124
If you like mathy vector speak, you can think of the above as an *inner product* of the two streams.
115125
By contrast, the `mergeMap` operator gives the *cartesian product* of two streams. That is, it gives us a way to take, for every element of a stream, a whole other stream, but flattened (or projected) together with the parent stream. The following enumerates all the row/column indices of cells in a spreadsheet:
@@ -132,7 +142,7 @@ columns.pipe(
132142
> ["C", 1]
133143
> ["C", 2]
134144
135-
![Mouse drag geometry](/assets/images/chapterImages/functionalreactiveprogramming/mergeMap.gif)
145+
![Merge Map Example](/assets/images/chapterImages/functionalreactiveprogramming/mergeMap.gif)
136146

137147
If we contrast `mergeMap` and `map`, map will produce an Observable of Observables, while mergeMap, will produce a single stream with all of the values. Contrast the animation for `map`, with the previous `mergeMap` animation. `map` has three separate branches, where each one represents its own observable stream. The output of the `console.log`, is an instance of the Observable class itself, which is not very useful!
138148

@@ -148,7 +158,7 @@ columns.pipe(
148158
> Observable
149159
> Observable
150160
151-
![Mouse drag geometry](/assets/images/chapterImages/functionalreactiveprogramming/mapmap.gif)
161+
![Map Map Example](/assets/images/chapterImages/functionalreactiveprogramming/mapmap.gif)
152162

153163
Another way to combine streams is `merge`. Streams that are generated with `of` and `range` have all their elements available immediately, so the result of a merge is not very interesting, just the elements of one followed by the elements of the other:
154164

@@ -164,7 +174,7 @@ merge(columns,rows)
164174
> 1
165175
> 2
166176
167-
![Mouse drag geometry](/assets/images/chapterImages/functionalreactiveprogramming/merge.gif)
177+
![Example of Merge](/assets/images/chapterImages/functionalreactiveprogramming/merge.gif)
168178

169179
However, `merge` when applied to asynchronous streams will merge the elements in the order that they arrive in the stream. For example, a stream of key-down and mouse-down events from a web-page:
170180

@@ -176,6 +186,8 @@ const
176186

177187
It’s a convention to end variable names referring to Observable streams with a `$` (I like to think it’s short for “$tream”, or implies a plurality of the things in the stream, or maybe it’s just because [cash rules everything around me](https://www.youtube.com/watch?v=PBwAxmrE194)).
178188

189+
We can analogously think of `mouse$` as an array of `MouseEvent` objects, e.g., `[MouseEvent, MouseEvent, MouseEvent, MouseEvent, MouseEvent]`, and then we can perform operations on this array just as we would with a typical array of values. However, rather than being a fixed array of `MouseEvent`, they are an ongoing stream of `MouseEvent` objects that occur over time. Therefore, instead of being a static collection of events that you can iterate over all at once, the Observable `mouse$` represents a dynamic, potentially infinite sequence of events that are emitted as they happen in real-time.
190+
179191
The following lets us see in the console the keys pressed as they come in, it will keep running for as long as the web page is open:
180192

181193
```javascript
@@ -186,7 +198,7 @@ key$.pipe(
186198

187199
The animation displays the stream as the user types in the best FIT unit in to the webpage
188200

189-
![Mouse drag geometry](/assets/images/chapterImages/functionalreactiveprogramming/keydown.gif)
201+
![Key Down Example](/assets/images/chapterImages/functionalreactiveprogramming/keydown.gif)
190202

191203
The following prints “!!” on every mousedown:
192204

@@ -198,7 +210,7 @@ mouse$.pipe(
198210

199211
The yellow highlight signifies when the mouse is clicked!
200212

201-
![Mouse drag geometry](/assets/images/chapterImages/functionalreactiveprogramming/click.gif)
213+
![Click Example](/assets/images/chapterImages/functionalreactiveprogramming/click.gif)
202214

203215
Once again this will keep producing the message for every mouse click for as long as the page is open. Note that the subscribes do not “block”, so the above two subscriptions will run in parallel. That is, we will receive messages on the console for either key or mouse downs whenever they occur.
204216

@@ -210,7 +222,7 @@ merge(key$.pipe(map(e=>e.key)),
210222
).subscribe(console.log)
211223
```
212224

213-
![Mouse drag geometry](/assets/images/chapterImages/functionalreactiveprogramming/keyboardclick.gif)
225+
![Keyboard Example](/assets/images/chapterImages/functionalreactiveprogramming/keyboardclick.gif)
214226

215227
<div class="cheatsheet" markdown="1">
216228

@@ -499,6 +511,40 @@ The advantage of this code is not brevity; with the introduced type definitions
499511

500512
As an example of *scalability* we will be using this same pattern to implement the logic of an asteroids arcade game in the [next chapter](/asteroids).
501513

514+
### MergeMap vs SwitchMap vs ConcatMap
515+
516+
In RxJS, `mergeMap`, `switchMap`, and `concatMap` are operators used for transforming and flattening observables. Each has its own specific behavior in terms of how it handles incoming values and the resulting observable streams. Here's a breakdown of each:
517+
518+
Lets consider three almost identical pieces of code
519+
520+
```javascript
521+
fromEvent(document, "mousedown").pipe(mergeMap(() => interval(200)))
522+
fromEvent(document, "mousedown").pipe(switchMap(() => interval(200)))
523+
fromEvent(document, "mousedown").pipe(concatMap(() => interval(200)))
524+
```
525+
526+
With `mergeMap`, each mousedown event triggers a new `interval(200)` observable. All these interval observables will run **concurrently**, meaning their emitted values will *interleave* in the output. In the animation, the `x2` occurs when two observables emit at a approximately the same time, and it cannot be visualized easily.
527+
528+
![Merge Map Visualized](/assets/images/chapterImages/functionalreactiveprogramming/mergeMapMouseDown.gif)
529+
530+
With `switchMap`, each time a `mousedown` event occurs, it triggers an `interval(200)` observable. If another mousedown event occurs before the interval observable finishes (interval doesn’t finish on its own), the previous interval observable is canceled, and a new one begins. This means only the most recent mousedown event's observable is active. This can be seen as the counter restarting every single time a click occurs, as our interval always emits sequential numbers.
531+
532+
![Switch Map Visualized](/assets/images/chapterImages/functionalreactiveprogramming/switchMap.gif)
533+
534+
With `concatMap`, each time a mousedown event occurs, it starts emitting values from the `interval(200)` observable. Importantly, if a second mousedown event occurs while the previous interval observable is still emitting, the new interval won't start until the previous one has completed. However, since interval is a never-ending observable, in practice, each mousedown event's observable will queue up and only start after the previous ones are manually stopped or canceled. Therefore, no matter how many times a click occurs, the next interval will never begin.
535+
536+
![Concat Map Visualized](/assets/images/chapterImages/functionalreactiveprogramming/concatMap.gif)
537+
538+
We can make an adjustment to this, where, we stop the interval after four items.
539+
540+
```javascript
541+
fromEvent(document, "mousedown").pipe(concatMap(() => interval(200).pipe(take(4))))
542+
```
543+
544+
![Concat Map Visualized w/ End](/assets/images/chapterImages/functionalreactiveprogramming/concatMap_take4.gif)
545+
546+
Unlike the previous example with a never-ending interval, in this case, each interval observable completes after emitting four values, so the next mousedown event's observable will queue up and start automatically as soon as the previous one completes. This setup ensures that each click's sequence of interval emissions will be handled one after the other, with no overlap, maintaining the order of clicks and processing each one to completion before starting the next.
547+
502548
## Glossary
503549

504550
*Asynchronous*: Operations that occur independently of the main program flow, allowing the program to continue executing while waiting for the operation to complete.
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)