Rx map is misleading. Marbles in Rx are misleading.
Guys from Rx are doing awesome job while documenting most of Rx operators with marble representation. Whole dedicated site with interactive representation of those diagram was created as well. And it's still confusing, because concept of high-order observables (a.k.a. observables that emits other observables that emit other observables that...)
Creation
If we create observable from array (or any other 'ish' object), we basically have two options how to proceed:
let array = [1,2,3,4]
let obs1$ = Observable.from(array) /*version 1 */
let obs2$ = Observable.of(array) /* version 2 */
From operator will do something, we would manually do in following fashion:
let array = [1,2,3,4]
let manualObs1$ = Observable.create((observer) => {
array.forEach(element => observer.next(element));
observer.complete();
})
Of operator will do instead straightforward value pass:
let array = [1,2,3,4]
let manualObs2$ = Observable.create((observer) => {
observer.next(array);
observer.complete();
})
So, from operator will emit multiple values, while of operator will emit single value. Both of them will complete themselves after emission. If we add typescript <3 types to definitions, it makes clear sense:
let array: number[] = [1,2,3,4]
let obs1$: Observanle<number> = Observable.from(array) /*version 1 */
let obs2$: Observable<number[]> = Observable.of(array) /* version 2 */
But there is absolutely no high-order observable at this point. We can produce flat observable, only difference is they will emit different type of value (array vs scalar) and different amount of them (single vs size-of-array times). By the way, if so far you are confused and have no idea what I'm talking about, it's time for shameless self bump to one of my previous article.
Mapping
Value transformation is common task. And as long as you only need to do some trivial work, as add 'Mr.' in front of all emitted names, map is what you need:
let arr = ['Jonh', 'Rob', 'Kyle']
let Obs2$ = Rx.Observable.from(arr)
Obs2$.map((x) => { return 'Mr. '+ x } )
.subscribe(x => console.log(x))
But what if you wish to map ech emitted value to new observable object? Well, welcome to hell, you have just created nested observable: observable that emits observabls. Consider following code, where we need to get some details about users:
console.clear()
let arr = ['Jonh', 'Rob', 'Kyle']
let details = [
{age: 20,id: 3, name:'Jonh'},
{age: 30,id: 5, name:'Rob'},
{age: 35,id: 8, name:'Kyle'}
]
let Obs2$ = Rx.Observable.from(arr)
Obs2$.map( (x) => {
return details[details.findIndex(entry => entry.name === x)]
})
.subscribe(x => console.log(x))
This works perfect, console will output mapped user details. Then wy to bother? Because this is laboratory example. In real life, user details will be stored in server side database, and you have to fetch them. So, whole procedure is asynchronous, and you have no results at moment of calling map. Instead of ready to use result you will get server observable that will resolve with value at some time in future. One simple line change in code will smash our pretty console log output:
Obs2$.map( (x) => {
return Rx.Observable.of(details[details.findIndex(entry => entry.name === x)])
})
.subscribe(x => console.log(x))
Now for each user in array you got observable. Things are getting complicated for us, because we wish to work with values not with observables... mergeMap (a.k.a flatMap), smarter map brother comes to the rescue. What's the difference? MegreMap will under the hood do the following for each sub-observable:
- subscribe to it
- provide mission values to defined project function
- output project function values as flat emission
- All in real-time, no waiting for child to complete before start processing new one
So, for each of our three observable emitted values mergeMap will issue new observble, subscribe to it, get it's values and expose them to outer world as flat line of events. Here and here is official documentation. For long time description and visualization was unclear for me, so thanks to draw.io magic even someone with such low design skills as I was able to create own diagrams for map:
And for flatMap:
The box named additional magic is actually another Rx operator applied under the hood: mergeAll (documentation here and here)
Another gotcha is comparison of flatMap with native then promise operator. Most scored answer on StackOverflow claims those two are equivalent. It is not quite true in my opinion because:
- Promise can emit one (and only one) value via resolve
- thenable outputs are executed always in order
Both above statements are not true for observables and flatMap operator, because each observable can emit as much values as it wants, and flatMap actually can shuffle outputted values, if their emission is overlapping:
That's all. If I missed something, let me know, I'll be more than happy to fix.
Epilogue
As usual, thanks to my wife for encouragement. And for the little one for day-by-day smile with 4 teethes.
Corrections and fixes by Wiola Budzińska