Open benlesh opened 5 years ago
Related, I think that the current Observable proposal, as it stands could be simplified quite a bit and I filed an issue there as well
Ahead of reading this: I'd greatly like to apologize from any perceived tone here. It's very hard to criticize someone else's hard work without it sounding a little bad in a few spots. That's really not my intent, so if reading any of this upsets anyone, I'm sorry, that's not my intent.
Really appreciate this foreward, the fact that you're conscious of how other people feel and the effort you put in. Since we've spoken in person before, I totally understand how kind you are as a person!
I also appreciate the detailed engagement, despite not entirely correctly, charitably or fairly characterising the proposal/effort. In it's current lengthy format, I think it would be fair to say it would be unproductive to attempt to respond to everything line-by-line here, but I propose the following breakdown:
A number of comments relate to mischaracterisation of Observables. We're working on some new docs that includes a section showing how you might do the same with Emitter as particular popular third-party libraries. I'd be happy to review this with you and we can remove anything that's not correct. Most of this prose will removed from here in favour of examples that will hopefully make it less subjective to different interpretations.
A number of comments relate to some bikeshedding. For example, untilNumber
, untilPromise
, vs having until(number)
, until(promise)
, etc. I think these are a great candidate to open as top-level issues and strongly recommend you try to convert as much into smaller, focused issues we can discuss on GitHub.
A number of comments relate to more general points which can be broken out too e.g. function composition, which I agree with, can explain more about, and what the challenges are around that. I'm sure you are probably aware too, and if there are suggestions you'd like to propose, it might be good to list the pro's as well con's of the different solutions.
A number of points about Emitter are not correct (e.g. cancellable-ish - .resolve()
, deterministic resource management - finally
lifecycle callback), but I assume this is largely due to my poor explanations here. Hopefully these will be clearer after new docs, and we can discuss offline or over a call to clarify/iterate.
A number of comments relate to feedback. It's not a great approach to be dismissive of all and any critical feedback, minimizing it as only "the author's feelings", implying their "not that smart" if they don't understand Observables, or we begin to debate how popular the metrics actually are, or listing every tweet/company/person/framework that has used/not-used/praised/criticised/backed out of using RxJS.
A number of comments relate to popularity. Everybody understands experimental feedback is important and part of the process. Right now at Stage 1 my approach is to ensure we agree on the major semantics so we later publish and promote something that is reflective of other member's view's too rather than just my own. There's already been lots of great changes. I'd personally much rather not publish anything at all, than go ahead and publish my own library, get adoption, and use that as a bargaining chip to try to force people to implement it. Popularity on it's own is also not the end goal, many libraries are not suitable for standardisation, get superseded (Angular 1), or often inspire language features, but not imported directly ($
vs querySelector/querySelectorAll
, or .?
).
Overall, I think you may be approaching this from the wrong perspective. There's already significant challenges to introducing any primitive here. Given that you've already revisited thinking about how you could change the Observable's proposal now, and it's even more similar to Emitter, it would be great to discuss the similarities beside the differences that remain, see which one's we can narrow done and work together on this proposal.
I think really I just need to see an open source reference implementation.
My primary concern: I think having a next
method, and forcing multicast, makes Emitter too high-level and makes the type less useful as a building block. If you remove that though, it's an Observable, and really we should be cooperating on that proposal.
My secondary concern is that we'll be throwing away more than a decade of knowledge and use around a very primitive type that has been proven useful, in order to standardize something untested that the community has not found use for yet.
But again, I'd really like to see an implementation of it.
@benlesh I agree with a lot of what you said, but unless I haven't understood correctly, I have to disagree with:
However, outside of the act of subscription, all observation (the important part) is completely unidirectional.
If you see this answer I gave on SO about a problem of Error: no elements in sequence
, you can see that what caused that was that first()
was called (by the consumer), but the stream was closed because a takeUntil()
(also called by a consumer) received an Observable
, and the Subject
that generated the Obsevable
received an error when called next()
(producer).
The error happens because first()
needs to receive at least one item before the stream closes, but the error is not received in an argument in the subscribe()
, but when next()
is called. This means that the producer received an error due to something done by a consumer, so it doesn't seem unidirectional to me.
Of course, this is related to an RxJS implementation, and first()
might not even be included in ECMAScript, even if Observable
is included, so this doesn't mean that a Observable
itself would be bidirectional; but in any case, given that RxJS ends up being the reference for Observables
in the javascript world, and that if the Observable
proposal is accepted, it will probably take the RxJS implementation as a reference.
That's still related to the subscription. Not the emissions. first, take, takeUntil, et al, all cause teardown. Which travels back up the chain. But that's related to the operator chains and how they subscribe. Setting up an observable subscription to unsubscribe is purely optional.
What's really interesting is that individual piece can also be modeled as an observable, as it's unidirectional. You can pass a hot observable to a subscribe call as a cancellation token. I'm not strictly talking about the rxjs implementation. I'm talking about observables at a high-level.
I think it's fine to travel up the chain, as long the producer (subject) doesn't fail or something of the sort.
That said, it seems that the error is not actually in the next()
(causing an error in the producer), and can actually be handled in the 2nd argument of the subscribe()
method (I've updated my answer on SO).
(I still think that first()
behaves in an unexpected way, but that's not about bidirectionality, and it can be circumvented with take(1)
)
Ahead of reading this: I'd greatly like to apologize from any perceived tone here. It's very hard to criticize someone else's hard work without it sounding a little bad in a few spots. That's really not my intent, so if reading any of this upsets anyone, I'm sorry, that's not my intent.
Responding to some of the claims about Observable
I want to clear up a few of these statements I found in this repo:
True
This is not true. Observables can handle individual and multiple consumers, as a biproduct of closures. Observables are an extremely low-level primitive, like a specialized function. For example:
Now, assuming that we have observable in the language, it's possible to make all of this more ergonomic by having EventTarget, et al, use that observable:
Handling multiple consumers is a compositional feature of Observable. You're not locked into "always multicast" or "always unicast". So the above statement is wrong and should be removed, IMO.
Observables error-handling semantics are not much different than that of Promises. So I think "invent new" needs to be explained here in great detail, or it's really just editorializing. "becomes a barrier/challenge" is also something that needs to be demonstrated and justified, in detail, against the challenges of current, existing types, such as callbacks and promises without editorializing.
To be clear, this is a type that has been in many languages and in JavaScript for more than a decade. The error semantics here are well defined and understood. It's not a new type that was created for a new proposal.
If Emitter is a "lower level abstraction" than Observable, it should lend itself to more use cases than Observable and cover more.
Observable:
Emitter:
While you might be able to create an Observable using functions an Emitter, I'm not sure it's going to be very efficient or useful to do so. However, I'm fully confident that with an Observable primitive in the language, an efficient Emitter library could be created as a third party thing.
This is inaccurate. Basically it is saying "it's not Subject, it's a subject and operators" in RxJS-speak. This totally works in RxJS, and has for 4-5 years now:
... that said, I'm not sure it's a great feature:
next
?)Without a reference implementation, this is tough to say. This "bidirectionality" you're speaking of here is really just the result of composition. Each one must subscribe to the previous. However, outside of the act of subscription, all observation (the important part) is completely unidirectional. So this difference might be a bit of a misnomer, or at least needs clarified (both in terms of what Observable is doing, and in terms of exactly how Emitter is not going to do this.
Again, this is highly editorialized and requires evidence or should be thrown out.
It's a byproduct of the compositional nature of functions more than anything. And a hallmark of the reusability of Observable. I don't see any how any type is going to be reusable without some sort of upward call. If you can compose an Emitter from other Emitters, the new emitter must call
run
(or whatever) on the other Emitters it's consuming. That's the "upwards call" you're claiming doesn't exist. Overall, I think this claim should be removed.Issues with Emitter in general
I touched on a lot of this above. But I'd like to summarize my concerns a bit.
until
is a bit more complex than returning aSubscription
, or even better, passing a cancellation token likeAbortSignal
or the like.until
is a highly polymorphic function that takes anumber
, or afunction
, or aPromise
, or anEmitter
. Nothing else in JS does that, other than something likeArray.from
, which in my opinion is a bit different. Withuntil
, you get different behaviors based on different values passed. While RxJS also does this in a few spots, I don't think RxJS should be in the language either.run
, for example) seems to fly in the face of better function composition proposals like the pipeline operator or bind operator.On the grounds of number 4 alone, I would strongly discourage standardizing this type, until more evidence of its usefulness is available.
At the highest level of this proposal, I'd like to steer clear of the author's feelings on how difficult Observable is to understand in comparison to this new Emitter type. I honestly think they're both hard to understand, however, all evidence is that RxJS, which is a beefy, heavy-handed, kitchen-sink approach to Observable, is something people are catching onto more and more easily, and the popularity is growing rapidly. 12.5M downloads a week on npm, used in several large well-known projects (YouTube, Netflix, Slack, PS4, et al), someone even organized a conference about it. None of that is to say Observable is "better", but it is to say that it's certainly does not seem difficult for a large, and growing, number of people to understand. I taught myself observables, and I've been teaching them to other people, and frankly, I'm not that smart.