WICG / observable

Observable API proposal
https://wicg.github.io/observable/
Other
563 stars 13 forks source link

`Iterator.from()` seems to be a thing now #28

Closed domenic closed 11 months ago

domenic commented 1 year ago

This proposal says

This appears in the TC39 proposal's README.md file but not the spec, so its fate is unclear. leftwards_arrow_with_hook

but https://tc39.es/proposal-iterator-helpers/#sec-iterator.from now exists, as does https://tc39.es/proposal-async-iterator-helpers/#sec-asynciterator.from .

benlesh commented 1 year 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).

ljharb commented 1 year ago

(note it's Iterator, not Iterable) I would definitely expect an Observable.from() as well.

benlesh commented 1 year ago

note it's Iterator, not Iterable

Yeah.. it's a strange design choice over there. But given that all iterators are iterable, potato-potahto.

ljharb commented 1 year ago

"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 :-)

syg commented 1 year ago

Iterator.from (and the rest of synchronous iterator helpers) is shipping in Chrome since 117.

benlesh commented 1 year ago

So the question becomes, what do you want Observable.from to convert?

In RxJS we handle:

domfarolino commented 11 months ago

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 :)

ljharb commented 11 months ago

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.

bakkot commented 11 months ago

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.

ljharb commented 11 months ago

I agree with that also :-) but i still think it should be in 262.

domfarolino commented 11 months ago

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 since Observable would be built into the platform a property of Observable 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:

  1. A seamless way to let language syntax hook into user-defined functionality without relying on typical string-defined property names
  2. 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(???) (i.e., "anything" can be an "iterator" without the language providing an 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).

ljharb commented 11 months ago

For Promises, it's the string "then", but for newer protocols we prefer Symbols.

domfarolino commented 11 months ago

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).

bakkot commented 11 months ago

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

domenic commented 11 months ago

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.

domfarolino commented 11 months ago

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:

  1. How important this is to them
  2. If it could appear as a follow-up

(Edit: Looks like @domenic just beat my message by a few seconds!)

domenic commented 11 months ago

I'll note that this from() method proposed is somewhat novel for the platform. The precedents are:

This 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.

domfarolino commented 11 months ago

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.

bakkot commented 11 months ago

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., awaiting any promises yielded by the sync iterable).

(AsyncIterator.from will not accept promises, just async iterators, async iterables, and sync iterables.)