scan
The last paragraph in scan's docblock says (emphasis is mine):
Returns an Observable that applies a specified
accumulatorfunction to each item emitted by the source Observable. If aseedvalue is specified, then that value will be used as the initial value for the accumulator. If no seed value is specified, the first item of the source is used as the seed.
However, the actual scan behavior isn't exactly like this:
...applies a specified accumulator function to each item emitted by the source Observable
It doesn't apply the accumulator function to every item if I don't set any seed. In that case it just passes the first emission through without calling accumulator:
from(['a', 'b', 'c']).pipe(
scan<string, number>((acc, value, index) => {
console.log(value, index);
return 0;
}),
).subscribe();
This will make the following output even when the first item was "a":
b 0
c 1
This is the relevant code:
https://github.com/ReactiveX/rxjs/blob/de9d2e4cef759b358c6c5d53b85e337dbf1c9c45/src/internal/operators/scan.ts#L98-L105
Demo: https://stackblitz.com/edit/rxjs6-demo-ynfnan
So even though the sequence started with "a" the accumulator function was called for the first time for "b". Also the index passed started at 0 for the second item (does index count how many times the function was called or does it count the number of items that went through this operator?).
the first item of the source is used as the seed.
From the last sentence I would guess that the accumulator function will be called even for the first value like this order (even though it is a little weird):
accumulator('a', 'a', 0);
accumulator(..., 'b', 1);
accumulator(..., 'c', 2);
... while in fact it's called like this:
accumulator('a', 'b', 0);
accumulator(..., 'c', 1);
So maybe I'm just dumb but the description and actual behavior without setting any seed really confused me.
You are correct and the documentation is wrong. The scan and reduce operators behave like Array.prototype.reduce when a seed is not specified.
That also means that the typing is incorrect. Fixing the types has been on my TODO list for some time.
@cartant I checked it again and documentation for Array.prototype.reduce and for the initialValueOptional param says:
Value to use as the first argument to the first call of the callback. If no initial value is supplied, the first element in the array will be used. Calling reduce() on an empty array without an initial value is an error.
and that's even when the description for reduce at the top of the page says (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce):
The reduce() method executes a reducer function (that you provide) on each member of the array resulting in a single output value.
So comparing scan to Array.prototype.reduce is probably correct.
I'm just not sure that if I don't set any seed for scan I should be expecting the first value to go through unprocessed. I'm also still a little confused that in this case index will start from 0 for the second item (I don't know if there's any other operator that behaves like this).
I realised that the behavior is a little different for mergeScan as well. mergeScan requires its seed to be always set so in never just passes through the first item as scan does. But I think it could work just like scan so I don't know if this difference in intentional.
yep, confusing, basically without a seed, scan just passes the event.
documentation bug
Please prioritize this, this is very confusing and just cost me multiple hours of debugging. I would hate for future developers to get stuck on this same documentation problem, considering it has already been discovered.
It is also confusing because the docs say
seed: Optional. Default is undefined.
Implying that if no seed is given, undefined will be used as a seed, i.e. the scan function will be called for the first item with undefined as accumulator (in reality it is not called for the first item).