Closed domenic closed 11 months ago
Nice. I think it's a welcome addition.
Being a push type, there's a lot more things that can be converted to observables. RxJS currently supports Iterables, Promises, AsyncIterables, and implementors of Symbol.observable
. (And ReadableStreams, but that's more because it was a small and easy addition, I don't know if I recommend that here or not).
(note it's Iterator, not Iterable) I would definitely expect an Observable.from()
as well.
note it's Iterator, not Iterable
Yeah.. it's a strange design choice over there. But given that all iterators are iterable, potato-potahto.
"Iterable" isn't a thing, it's a protocol with no shared pieces, whereas Iterator is already a thing that has shared pieces of the protocol, so it wasn't much of a choice :-)
Iterator.from
(and the rest of synchronous iterator helpers) is shipping in Chrome since 117.
So the question becomes, what do you want Observable.from
to convert?
In RxJS we handle:
Constructing Observables from Iterables, AsyncIterables, Promises, and other Observables definitely makes sense to me.
I'm curious how Symbol.observable
fits in here though. Is there any appetite to actually turn that into a well-known symbol, and is that even a possibility if we're not standardizing in TC39? Actually regardless of that, I'm not sure how @@observable
would look; I guess it'd expose a new "Observable" interface that's very similar to the Iterator interface (but with a way to "subscribe" I guess), with the language itself not really using it at all. That is, no syntax would hook into Symbol.observable
, and the interface it exposes would only be useful to Observable.from()
? That kind of feels like the wrong usage of a well-known Symbol, but then again I probably have the least knowledge of these out of all of us :)
It's technically possible for the web to add a well-known Symbol, but if that's a goal then that even further cements that TC39 is the proper venue for this proposal.
Personally I would like to get away from sticking all well-known symbols on Symbol
anyway. IMO it makes more sense to hang this one off of Observable
.
The Symbol.observable
convention made sense when there were many libraries which needed to coordinate on something not library-provided for interop, but since Observable
would be built into the platform a property of Observable
would work just as well for that.
I agree with that also :-) but i still think it should be in 262.
I'd like to not use this issue as an opportunity to debate standardization venue again (hence my "Actually regardless of that" above).
The
Symbol.observable
convention made sense when there were many libraries which needed to coordinate on something not library-provided for interop, but sinceObservable
would be built into the platform a property ofObservable
would work just as well for that.
This is interesting, I want to make sure I understand it better. My misunderstanding is probably centered around simply not knowing the purpose of some of the well-known symbols in the first place. The rough reason for their existence that I had in my head was two-fold:
Iterator
interface to subclass)It seems like Observables are maybe orthogonal here? That is, (1) there's no language-level syntax that hooks into Observable behavior, and (2) once we actually land Observables for real, this weird, loose concept of an Observable "interface" (some random object that @@observable
would produce that has a subscribe()
property on it) shouldn't actually be needed, since the platform would have the real thing. I initially envisioned a well-known symbol being useful for backwards-compatibility for existing code, but I don't think that actually works, and it doubly doesn't work if to take advantage of it you'd have to hook into a brand new place (i.e., Observable.observableSymbolHere
).
Is the goal to make it so that developer-provided Observable implementations can expose their "observable protocol" on demand (by implementing the new symbol method that hangs off of Observable
), for sole usage of turning it into a "proper Observable" via Observable.from()
? In that case, where is Promise.promiseSymbolHere
, so that any thennable can be adapted into a platform promise? (I mean, I understand that await
works for all thennables, but what's missing here is a language-provided way to automatically adapt non-platform promises into "proper Promises", but that seems like a weird goal).
For Promises, it's the string "then", but for newer protocols we prefer Symbols.
So this is not about backwards compatibility, but instead just allowing any user-provided implementation of an "Observable protocol" be adapted into a platform Observable on demand, is that right? I guess that just feels weird if the only user of this functionality would be Observable.from()
, but maybe I'm wrong.
(Like, just subclass native Observable
then! Or is it the case that whenever we introduce a new interface we want some developers to be able to subclass it, and others to be able to "adapt" into it via a symbol-identified type-less "protocol"?... that is the part that feels weird to me).
A seamless way to let language syntax hook into user-defined functionality without relying on typical string-defined property names [...] A way for developers to provide functionality that implements an interface-like thing, without requiring the platform to provide a constructible version of that interface(???)
Not just syntax - lots of non-syntax things use the iterator protocol, for example, and none of the random RegExp-related symbols are used for anything syntax-based (IMO those should also not exist, but whatever).
The second part is accurate though not really separate from the above.
That is: symbols are the right tool any time you want to define an interface which would be consumed or implemented by things outside of your code, whether or not that interface is used by syntax. There's even a proposal to make that easier, though it's not relevant here (except that, like my suggestion, it would put its symbols on something other than the Symbol
namespace).
Is the goal to make it so that developer-provided Observable implementations can expose their "observable protocol" on demand (by implementing the new symbol method that hangs off of Observable), for sole usage of turning it into a "proper Observable" via Observable.from()?
Well, I don't particularly have that goal, but some people might, and if so a new symbol would be the way to go. As mentioned above it doesn't have to be syntax-related to warrant a new symbol.
I guess that just feels weird if the only user of this functionality would be Observable.from(), but maybe I'm wrong.
If this functionality exists, I think you'd want anything which accepts an observable to look for the symbol, the same way anything which accepts an iterable looks for Symbol.iterator
. This behavior would apply at the very least to the takeUntil
method. This would follow immediately from "have Observable.from use the symbol" + https://github.com/domfarolino/observable/issues/44, though you don't need all of #44 to get this behavior.
To be clear, my opinion is
Observable
class, rather than living on Symbol
I would punt on accepting userland observables until we have some significant experience with platform observables in the wild. In retrospect, I think we put way too much effort into making platform promises interoperable with userland ones, and it was a mistake that caused a lot of complexity in the design, for something that was only useful for a brief transition period.
In particular, if we see a lot of people writing myObservableToPlatformObservable()
helper functions, and using them, even after a year or so of a transition period, then I would revisit this issue. But I wouldn't assume from the beginning that this functionality is necessary.
Thanks a lot @bakkot, I think that really helped me understand what's going on here. I guess it'd be nice to hear two things from others / the community:
(Edit: Looks like @domenic just beat my message by a few seconds!)
I'll note that this from()
method proposed is somewhat novel for the platform. The precedents are:
Array.from
/ %TypedArray%.from
: accepts array-like and iterableReadableStream.from
: accepts async iterable (which automatically falls back to iterable, and also automatically accepts ReadableStream
since ReadableStream
is async iterable)Iterator.from
: accepts iterables or iteratorsAsyncIterator.from
: accepts async iterables or async iteratorsThis is the first time we're adding something which does such a grab-bag of conversions. Probably that's fine? But, for example, AsyncIterator.from()
does not accept a promise, so so Observable.from()
doing so is expanding the meaning of from()
methods a bit, and might expand the expectations people have of them.
It looks like AsyncIterator.from
also falls back to sync iterables, given its delegation to @@iterator
in https://tc39.es/proposal-iterator-helpers/#sec-getiteratorflattenable? I'm not sure if that's correct though, since https://tc39.es/proposal-async-iterator-helpers/#sec-asynciterator.from passes in the string "async" to GetIteratorFlattenable(), and that value never seems to be considered.
That's a good point about expanding the meaning of .from()
generally. I think I'm convinced that the big parallel drawn between Promises and Observables could justify this convenience, but if there's a real concern we're setting a bad precedent then I am open to leaving it out initially.
It looks like AsyncIterator.from also falls back to sync iterables, given its delegation to
@@iterator
in https://tc39.es/proposal-iterator-helpers/#sec-getiteratorflattenable? I'm not sure if that's correct though, since https://tc39.es/proposal-async-iterator-helpers/#sec-asynciterator.from passes in the string "async" to GetIteratorFlattenable(), and that value never seems to be considered.
The spec for async iterators is currently stale while I am in the process of rewriting all of the underlying mechanics in that proposal, so don't worry too much about what it says. I can confirm that the fallback to sync iterables will still be there. It matches for await (item of syncIterable)
. IMO every consumer of async iterables should fall back to looking for a sync iterable and doing the same async-from-sync conversion that for await
does (i.e., await
ing any promises yielded by the sync iterable).
(AsyncIterator.from
will not accept promises, just async iterators, async iterables, and sync iterables.)
This proposal says
but https://tc39.es/proposal-iterator-helpers/#sec-iterator.from now exists, as does https://tc39.es/proposal-async-iterator-helpers/#sec-asynciterator.from .