-
Notifications
You must be signed in to change notification settings - Fork 34
Questions regarding "auto-flatten" an non-iterables #275
Comments
Iterables are ignored by design in array flatMap (because strings are iterable), only In async helpers, I'd expect that also, an async iterable or iterator is flattened. |
Sorry, I misread your comment initially. Let me try again: First, there's no changes to Array.prototype.flatMap in this proposal. Second, there's a summary of the behavior at #233, which is correct except for the caveat that
For This is inconsistent with
Not sure what this is asking that is different from the previous question so I don't know how to answer it.
Its implementation is an Array-specific one-off. The general principle we decided on at the time was "X.prototype.flatMap should only flatten Xs". We then immediately made an exception to allow iterator flatMap to also flatten iterables, but that's still the idea. |
@bakkot What about promises in an
Context: I'm asking all of these things because RxJS deals with a LOT of similar issues. I'm currently discussing what changes we might need to migrate towards to come to some sort of alignment here In RxJS, for a very long time we've supported "auto converting" anything we can convert to an Observable. Including strings, but my hot take for you there is that usually when someone returns a For example, in RxJS, all of these "just work": someObservable.pipe(concatMap(() => [1, 2, 3])); // Observable<1 | 2 | 3>
someObservable.pipe(concatMap(async () => 42)); // Observable<42>
someObservable.pipe(concatMap(() => Promise.resolve(42))); // Observable<42>
someObservable.pipe(concatMap(function* () {
yield 1;
yield 2;
yield 3;
}); // Observable<1 | 2 | 3>
someObservable.pipe(concatMap(async function* () {
yield 1;
yield 2;
yield 3;
}); // Observable<1 | 2 | 3> By and large, it's been a benefit to the community. In particular the handling of promises. However, the |
@ljharb I presume that |
No others, and if primitive Tuples were to be added, they'd likely be allowed instead of throwing, but it's more the consistency point underscoring that making strings iterable in the first place was a huge mistake. |
That's an interesting case. I'm not sure what people would mean if they were like RxJS could benefit from this sort of spec-ing, an honestly it might serve the observable proposal a bit if we did this sort of thing. So I'm trying to learn here. |
Still curious about this:
And further, I'm not totally sure what this would do: someAsyncIterable.flatMap(async function* () {
yield* someOtherAsyncIterable;
})
// vs
someAsyncIterable.flatMap(async function* () {
yield someOtherAsyncIterable;
}) I presume they'd return |
AsyncIterator's (I should note that this only applies to In particular, just like Concretely: someAsyncIterable.flatMap(async* () => {
yield 'hi'
})
// ["hi", "hi", ...]
someAsyncIterable.flatMap(async () => {
return 'hi'
})
// throws, since the result is a Promise, which is not iterable or an iterator in either the sync or async sense
someAsyncIterable.flatMap(() => 'hi')
// throws, since the result is a string
someAsyncIterable.flatMap(() => Promise.resolve('hi'))
// throws, since the result is a Promise, which is not iterable or an iterator in either the sync or async sense But, this does work, since the result is iterable: someAsyncIterable.flatMap(() => ['hi'])
// ["hi", "hi", ...] So does this, and it will unwrap the promise just like someAsyncIterable.flatMap(() => [Promise.resolve('hi')])
// ["hi", "hi", ...] - note that the promise is unwrapped
Yeah. Incidentally Tab has a good essay about it being a mistake to have made String iterable in the first place. Though note that
someAsyncIterable.flatMap(async function* () {
yield* someOtherAsyncIterable;
})
someAsyncIterable.flatMap(async function* () {
yield someOtherAsyncIterable;
})
Correct. |
Great! Thank you for answering these questions. Very helpful. |
@benlesh Oh, one thing I just noticed - your examples were writing That is, you're not going to be able to do That's also why this isn't a breaking change for Array: |
That's interesting. I guess it makes sense. Just to jump to my cases around this (which are admittedly off-topic for this repository)... Observable is the "dual" of Iterable, such that it's really an iterable turned inside out. So thinking about iterator having the methods is a bit mend bending. The transformation from Iterable to Observable would be something like as follows (assuming a more "classic" non-JS iterable): interface Iterator<T> {
next(): T;
done(): boolean
}
interface Iterable<T> {
iterate(): Iterator<T>
}
//////// "DUAL" ///////////////
// Just pass the value instead of returning it, really.
interface Observer<T> {
next(value: T): void;
done(isDone: boolean): void;
}
interface Observable<T> {
observe(observer: Observer<T>): void;
} Plus you need some sort of cancellation semantic to remove the It makes me wonder about what it would look like to put the methods/operators on Regardless, it brings me to another question about this: Considering an function* generateNumbers() {
for (let n = 0; n < 10; n++) {
yield n;
}
}
const gen = generateNumbers()
// Iterate two values off of the iterator
gen.next();
gen.next();
// NOW map it.
const whatWillHappen = gen.map(n => n + n)
for (const value of whatWillHappen) {
console.log(value); // What does this log??
} I'd presume that it would log It's an interesting foot-gun. Although I suspect that most people will be a victim of iterating once, then mapping and trying to iterate it again, which is a known foot-gun with iterators regardless. |
Yeah. Though I don't know that the "dual" idea entirely holds in the way which is relevant here - Iterable and Iterator are both conceptually sources, whereas Observable is conceptually a source and Observer is conceptually a sink. And these methods make more sense on source than sinks (though you could put them on sinks, in principle, and have them apply before the value reaches the sink). On the one hand, I do think it is kind of unfortunate that it's not practical for us to put methods on an abstract Iterable class - but on the other hand, Iterable is in practice more interface-like than Observable is, and having a concrete class (rather than an interface) to have the methods is nicer in some ways also. That is, specifically, calling
Yup. It'll be really important to internalize that iterators are single-use, and that calling one of the helper methods constitutes a "use" just as much as iterating it would. |
Oh... To be clear, there are ways you can create all of the same APIs over Observer. And they're single use too, it's just that they're often stateless. Duality still applies. // In this theoretical API:
const observer = new Observer()
.map(x => x + x)
.filter(x => x >= 10)
.reduce((acc, x) => acc + x, 0)
.handle(console.log)
// someObservable emits 1,2,3,4,5,6,7,8
// console logs 52
someObservable.observe(observer) Weirdly, at this point, in RxJS they would be import { Subject } from 'rxjs';
const subject = new Subject<number>().pipe(
map(x => x + x),
filter(x => x >= 10),
reduce((acc, x) => acc + x, 0)
)
subject.subscribe(console.log)
// someObservable emits 1,2,3,4,5,6,7,8
// console logs 52
someObservable.observe(subject) |
Perhaps it should be mentioned in the README that the callback function given to flatMap always must return an iterator? Or how MDN says: it must return "a new iterator helper". I was surprised the following doesn't work: [1, 2, 3].values().flatMap(num => num === 2 ? [2, 2] : 1).toArray() |
@mb21 You are quoting the return value of
...which is wrong. I thought it works like |
@Josh-Cena right, thanks! I made a PR over there: mdn/content#33475 |
Terribly sorry if this is clearly documented and I missed it
With
Array#flatMap
, it will automatically flatten/merge in values that are not arrays into the array output:However, it will NOT auto convert any old iterable (I know this is a separate thing, just calling out the behavior):
I'm using this information to help guide the direction of RxJS observables and our operators/methods long-term.
Questions
Iterable
orAsyncIterable
?Iterable
orAsyncIterable
? Will they flatten anything that implementsSymbol.iterator
orSymbol.asyncIterator
respectively?Array#flatMap
? Or is its implementation an Array-specific one-off from the iterator helpers?Thank you in advance.
The text was updated successfully, but these errors were encountered: