Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove forEach and add "subscribe" overload #97

Merged
merged 5 commits into from
Jun 13, 2016
Merged

Conversation

zenparsing
Copy link
Member

@zenparsing zenparsing commented May 27, 2016

The easiest way to see the changes are just to look at the README diff.

Add a "closed" getter to Subscription and SubscriptionObserver

Combinators frequently need this information in order to correctly deal with synchronous dispatch use cases.

Remove the "forEach" method

The forEach method is not fundamental; it can be implemented on top of the API just like "filter" and "map". Also, it doesn't help with regard to a primary use case, DOM events, where we want the subscription object:

let subscription = element.on("click").subscribe(fn);
subscription.unsubscribe();

Add an overload to "subscribe" which accepts 3 function arguments

This makes observing a DOM event stream, for instance, very ergonomic:

element.on("click").subscribe(event => console.log(event.x));

@benjamingr
Copy link

I think dispose might be a better word than cancel here since cancel implies we're cancelling something and in my experience that's usually not the case with unsubscribe - this also might conflict with cancellable promises .cancel which is now also a stage 1 proposal and with your own cancellation token proposal.

That said - observable unsubscription is in-line with cancellation semantics.

+1 on removing forEach,
+1 for three function overload.
+1 for closed getter.

@zloirock
Copy link
Contributor

O_o

@zenparsing
Copy link
Member Author

@zloirock : )

This PR is meant as a discussion starter, more than anything else. These changes represent my current "final state" thinking.

@Blesh
@jhusain

@zenparsing
Copy link
Member Author

Also, @domenic

@zenparsing
Copy link
Member Author

@benjamingr JS doesn't have a concept of "disposable" which is tied to resource cleanup like C# does. In a possible future we might want syntax for resource cleanup, in which case we'll probably want to introduce a symbol-named method for that. Symbol.dispose would be a great choice.

@benjamingr
Copy link

benjamingr commented May 27, 2016

@zenparsing yes, but when you unsubscribe from an observable you're performing resource cleanup - like removeEventListener rather than cancelling an action.

JS also doesn't have a concept of cancellation yet, so I'm not sure if I buy the "JS doesn't have a concept of" argument when JS doesn't have a concept of both things.

@zenparsing
Copy link
Member Author

@benjamingr I guess I fail to see the distinction between unsubscription and cancelling an action, if you view the "action" as subscribing (whatever that might entail).

@zenparsing
Copy link
Member Author

Reverted the renaming of "unsubscribe" to "cancel", should be considered separately, if at all.

@domenic
Copy link
Member

domenic commented May 27, 2016

These changes represent my current "final state" thinking.

This doesn't really help with the two concerns raised at the TC39 meeting, namely that the close/error semantics should be built on promises and that cancelation should be built on cancel tokens. But maybe it is a stepping stone for getting there...

@benlesh
Copy link

benlesh commented May 28, 2016

I'm in favor of this, as I'd say 90% of people use RxJS observables with the subscribe(fn, fn, fn) overload, and I haven't seen much forEach usage.

@benlesh
Copy link

benlesh commented May 28, 2016

This doesn't really help with the two concerns raised at the TC39 meeting, namely that the close/error semantics should be built on promises and that cancelation should be built on cancel tokens.

Cancellation Tokens are a cool idea. I'd want to explore it more, because they do incur some rather unergonomic boilerplate.

Handling completion and error in Promises I think is a no-go. That means that entirely sync observables are forced to handle errors asynchronously. It also means that errors will be caught in the promise trap, current incarnations of Observable actually throw. This is because unlike promise, which only has then for both mapping and side-effects, observable can have map and subscribe, and the "end" of the "chain" can be identified programmatically. This enables observables to actually throw unhandled errors, rather than trap them like promises.

The promise cancellation design I saw recently needs a lot of work IMO. Tying observable to it will likely be a boat anchor. It's tying a new, untested, barely-vetted design of promise cancellation against a design that seems to have circled back to something that has existed for the better part of a decade in a consistent form.

I'll grant that promise cancellation is mildly similar to .NET's Task, but the similarities stop at what IMO is the only solid feature, the CancelationToken.

👍 supporting CancellationTokens
👍 three function overload
👎 promise as only path of error handling and completion handling
👎 cancellable promise

@benjamingr
Copy link

@Blesh cancellation is going to be based off tokens from what I understand.

I also don't understand how error handling and completion can be done synchronously through a then without breaking the synchronous use case.

@domenic
Copy link
Member

domenic commented May 29, 2016

The committee discussion went roughly as follows. The impetus for standardizing observables, instead of leaving them as a library, is so that they can be used in other parts of the platform. (This is true of any data structure, and is why promises were standardized.) The web already has one data structure for signaling success/failure; what it could benefit from is one for repeated invocation, i.e. the "next" part of observables. Any DOM observables, per the document in this repo, would not even use the success/error parts. As such, it's a requirement of the Chrome team that observables layer cleanly with the platform's existing primitives for success/error, promises, if they are to become part of the JavaScript language and not simply a library.

You mention "entirely sync observables", and that is quite worrying, as it reveals a disconnect between how this proposal has been presented to committee and how you are thinking about it.

@benjamingr
Copy link

You mention "entirely sync observables", and that is quite worrying, as it reveals a disconnect between how this proposal has been presented to committee and how you are thinking about it.

Indeed. Observable.from and Observable.of have been made synchronous recently and in general the proposal has been moving towards a direction where observables are not inherently asynchronous.

I think it's important to involve related parties since Google, Microsoft and Netflix are all heavily invested in observables and their design.

If Angular is rooting for zones and these sort of observables on one end and the Chrome team is rooting for different observables perhaps they should discuss the topic among themselves first.

@domenic
Copy link
Member

domenic commented May 29, 2016

The topic has been discussed internally and our findings presented at the last TC39 meeting; I am reporting back to this repo.

@benlesh
Copy link

benlesh commented May 29, 2016

it's a requirement of the Chrome team that observables layer cleanly with the platform's existing primitives for success/error, promises, if they are to become part of the JavaScript language

Why does the Chrome team dictate requirements? What makes that team any more important than the countless developers using the type?

@domenic
Copy link
Member

domenic commented May 29, 2016

They're not more important (except insofar as we would be the ones implementing it). We all try to come to consensus, which means accommodating everyone's requirements. If that's not possible, things don't advance, but hopefully it won't come to that.

@domenic
Copy link
Member

domenic commented May 29, 2016

Keep in mind these are the requirements for standardizing and implementing in the browser. We have no desire to change how developers use the type they're already using; they can continue doing that with no need to involve our requirements. We just need to be part of the consensus process if we're going to standardize something in the committee we sit on and implement it in the browser we ship.

@benjamingr
Copy link

@domenic the reason I'm pondering is because the Angular team has been pushing observables pretty hard (for example, @jeffbcross and @robwormald). The requirements collected in the es-observable repo are largely driven by what they're asking for and it's strange to learn that they're being blocked by the same team.

This is of course, no criticism towards you at all - you are the messenger of news and I appreciate these updates when the TC meeting results are less accessible to developers than ever (I don't know where to read meeting nodes, ESDiscuss is noisy and abandoned and only the ecma262 repo is reliably updated).

@jeffbcross
Copy link

HI I guess I'm in this thread now :).

The requirements collected in the es-observable repo are largely driven by what they're asking for

News to me. Maybe some of us have expressed preferences or use cases for consideration, but there's a lot more prior art than Angular 2 driving this spec.

and it's strange to learn that they're being blocked by the same team.

What is the Angular team blocking?

@benjamingr
Copy link

@jeffbcross Hi! Well, a lot of the time we've discussed reasoning based on the Angular 2 use case - examples from it for examples. @Blesh is a very active participant here and was involved in the calls in Angular 2 and so on.

What is the Angular team blocking?

Well, the Chrome team is asking for observables that work with promises which means they report completion through a promise (think subscribe returns a promise for the completion (or rejection) which can be cancelled through a token rather than a subscription with an unsubscribe).

@Blesh and others are asking for observables to work like in Rx - in that they allow you to emit values synchronously - this means no unbounded buffers and other nice things but it breaks some other guarantees (like code execution order never changing). According to these people writing observables into JavaScript the way @domenic describes would make observables useless.

Since you are a major consumer of Observables (Angular 2) and the last TC meeting consensus virtually means breaking every single bit of Angular 2 code using an Observable service (changing the signature of .subscribe). What I find odd is - you work at the same place as the team who rejected the observables design (after you carefully evaluated alternatives and picked it yourself).

And I quote you:

The core team is going to take some ownership of educating the community on observables, similar to how we helped in a small way educate on promises. We think there's enough value in composition and overall simplicity that it's worth making this a preferred first-class primitive. We also plan to help move the Observable TC39 spec forward however we can.

Now, you guys all know each other which is why I found it odd you're asking for conflicting requirements - that's all.

@domenic
Copy link
Member

domenic commented May 29, 2016

Note that we have no opposition to emitting values synchronously; that's in fact required for parity with DOM events. (Additionally, cancel tokens allow synchronous cancelation of the value emission.) It's the completion/error signals that are at issue here.

@robwormald
Copy link

I'm in favor of this, as I'd say 90% of people use RxJS observables with the subscribe(fn, fn, fn) overload, and I haven't seen much forEach usage.

I agree, I have yet to see a single line of userland angular2 code that uses the object signature.

If Angular is rooting for zones and these sort of observables on one end and the Chrome team is rooting for different observables perhaps they should discuss the topic among themselves first.

We'd love to do that...

The topic has been discussed internally and our findings presented at the last TC39 meeting; I am reporting back to this repo.

...that's very exciting, and we'd love to share our users' nascent experiences with Observable w/ Chrome.

The requirements collected in the es-observable repo are largely driven by what they're asking for and it's strange to learn that they're being blocked by the same team.

I refer you to https://twitter.com/_alastair/status/734154161705603072 (and also that we've been pretty quiet on the spec itself, though perhaps that's our error and now you will likely not hear the end of me...)

RE: spec changes:

  • 👍 on the overload.
  • ¯_(ツ)_/¯ on forEach removal, though it seems like its what Chrome actually wants with regards to Promisey delivery of complete/error notifications, no? (though not for cancellation...)
  • 😕 on the rest, mostly because its not clear what the alternative approach / concerns @domenic is talking about would look like.

Hypothetical example (this is trivial in RxJS today, so this is the "bar" for me) that might elucidate the use case (and represents the kind of interop I'd really like to see)

Rx5 (using an Observable-based XHR wrapper, using the current cancellation semantics)

//grab a DOM element
const searchInput = document.getElementById('searchInput');
//get an Observable of the input events
Observable.fromEvent(searchInput, 'input')
//map out just the text
  .map(ev => ev.target.value)
//switchMap (nee "flatMapLatest") to make an http request 
// *and cancel the in-flight request if a new value is emitted upstream before the previous request completes*
  .switchMap(searchText => Rx.Ajax.get(`http://api.com?search=${searchText}`))
  .subscribe(
    response => console.log(response),
    err => console.log('err', err)
  )

Let's imagine a future awesome-world where fetch is cancellable (by whatever means) and Observable is a first-class thing - this is what it should look like in my dreams.

searchInput.on('input')
  .map(ev => ev.target.value)
  .switchMap(url => fetch(`http://api.com?search=${searchText}`))
  .subscribe(
    response => console.log(response),
    err => console.log('err', err)
  );

Is such an API possible with the cancellation-token based Promises proposal + the TC39 recommendations for Observable? I realize this is a userland concern but this sort of thing is why Angular2 uses Observables (and why we're not currently using a lot of fetch)

@benjamingr
Copy link

What @domenic is discussing would hypothetically look like this:

searchInput.on('input')
  .map(ev => ev.target.value)
  .switchMap((url, token) => fetch(`http://api.com?search=${searchText}`, token))
  .subscribe(response => console.log(response))
  .then(done => console.log("Done, this shouldn't fire on an input in this case")),
  .catch(err => console.log('err', err));

The issue here is whether or not it's fine for the callbacks that signal observable completion or error to fire synchronously.

@benlesh
Copy link

benlesh commented May 31, 2016

His rhetoric was right on, if a little pointed. You should probably just read the rest of the post and take it seriously. There are reasons this design illicits a negative response. The "I don't like your tone, so that invalidates your points" is a fallacy.

@domenic
Copy link
Member

domenic commented May 31, 2016

I'll try and make time to wade through it later this week, but given this community's unwillingness to engage constructively, perhaps it's better that observables stay in userland. If there are fundamental conflicts between them and the rest of the primitives in the web platform (as claimed), and the community is not only unwilling to address them but is railing against any attempts to do so, then we simply can't build a solid collaborative standards foundation on this kind of interaction.

@staltz
Copy link

staltz commented May 31, 2016

How is the conversation not constructive if we're supposed to either "Show a convincing case for why synchronous completion/error is a requirement." or accept the cancellable Promise foundation, if what I did was precisely show convincing cases for sync completion/error?

PS: I don't think the rhetoric I used is so unfamiliar to you. ;) It's the least to expect after solid examples are dismissed with "disturbing".

@benjamingr
Copy link

@staltz

It's not disturbing, it's battle-proven. I see ES Observable as a proposal to standardize that which is proven to work well in hundreds of codebases with RxJS.

Hundreds of code bases in RxJS isn't a large amount at all. In general, RxJS has not seen wide adoption until very recently - and that's arguably because it is being sold as part of the spec. I've been using Rx for a while and there hasn't ever really been a stable API surface since v2.x. The library has been changing a lot and it's still beta.

That said, the .subscribe(onNext, onError, onComplete) is everywhere, it's not just in every single version of RxJS I'm aware of. I definitely agree with the sentiment that we should standardize based on it - similarly to how promises were standardized with a then method.

The proposal built on Promises is the disturbing one because it is unproven.

👍

It totally ignores reality in favor of a Promises-ruled fantasy land, making a more awkward and less useful API just to satisfy some peoples' aesthetic preferences that aren't even applicable to real code.

That's entirely unfair. That's not the argument anyone is raising.

Observables are not "sync collections", but they aren't "async collections" either. Array is a "necessarily sync collection", Observable by the traditional Rx sense is "not necessarily sync collection".

I think the easy way to explain this is that observables are like iterables and not like arrays. They're not even a collection to begin with - they're push streams.

Maybe given the bottom output in that diagram, I would need to apply exhaustMap, then we get the complication described in the previous comments.

To be fair, exhaustMap on synchronous observables is kind of an edge case. I've been using Rx for years and I haven't been able to come up with a real use case where I would use it with two synchronously exhausted observables for it other than to explicitly differentiate what is sync from what is not which is sort of an Rx anti-pattern IMO.

I'm sure though that we can come up with more examples where the promise-based proposal breaks.


@staltz

On another note, I agree with Domenic that this sort of rhetoric makes it hard to interact. If we ever want to see native observables (and I do) we need to interact in a much less intentionally argumentative tone.

You don't like promises - that's fine but the TC does like promises, WHATWG and browsers are major consumers of promises and NodeJS is working on a promised API. Promises are generally seeing a very high adoption rates, language syntax already implemented in two browsers (async/await), new cancellation syntax and new native APIs.

On the other hand, observables have not seen any platform adoption at all. I'd like to see that change. In order for that to change we need to work with WHATWG, we need to work with the TC and we need to work with the community. Just having a library we think is awesome does not justify spec inclusion.

You have a lot of very smart things to say, you're a library author of a really cool library and are an Rx core contributor - you have a stake in this and your opinion is valued. If you don't say them in a way that makes interacting with you possible for the TC and DOM API stakeholders they won't listen to you and your feedback won't be considered. Domenic will have these discussions in private with jhusain and zenparsing without involving you or me in the process. This can in all likelihood slow down the process even further which is a real shame since we've been at this for over a year now and the proposal is still stage 1.

@staltz
Copy link

staltz commented May 31, 2016

@benjamingr

promises-aplus/promises-spec#94 (comment)

Yeah this is really not happening. It totally ignores reality in favor of typed-language fantasy land, making a more awkward and less useful API just to satisfy some peoples' aesthetic preferences that aren't even applicable to JavaScript.

;)

@benjamingr
Copy link

benjamingr commented May 31, 2016

@staltz ok that's pretty funny.

Aside from the lols - you're criticizing the way a decision was made but you want to make one the same way.

(Unrelated - in that case Domenic was fending off people who have no experience with the spec or language. In this case we're talking about the language technical committee itself - so it's hardly comparable)

@benjamingr
Copy link

Also, I would like to see more use cases aside from exhaustMap that justify it. I've only seen a single code base break from the change with exhaustMap.

Also note, we're arguing about new Observable which I almost never ever use in code. A lot of people I know aren't even aware they can make observables this way.

@domenic what about something like a Observable.isComplete that is set synchronously? That way we don't need any built-in synchronous callbacks but can still observe completion and error synchronously. That would solve the exhaustMap issue.

@benlesh
Copy link

benlesh commented May 31, 2016

this community's unwillingness to engage constructively,

I think that's probably a two way street. Come on back to the table. I'd like you to look at #99

@domenic
Copy link
Member

domenic commented May 31, 2016

@domenic what about something like a Observable.isComplete that is set synchronously? That way we don't need any built-in synchronous callbacks but can still observe completion and error synchronously. That would solve the exhaustMap issue.

Yes, something along those lines makes a lot of sense to me. Synchronous promise inspection as a recipe for integrating promises and observables was one possibility we discussed with the champions at TC39. We hadn't considered exposing it directly, but instead simply using it to ensure that next() cannot be called after the promise is resolved or rejected, but it of course makes sense that if it's something the standard would need, it's something that libraries would want as well. In this case it would be returnValue.inspect().state !== "pending" or returnValue.state !== "pending".

@benlesh
Copy link

benlesh commented May 31, 2016

Observable.isComplete doesn't make sense because observable is just a function to tie an observer to a producer. It can be subscribed to many times. Observer.isComplete would work, but only if we leaked the observer out to the public, so that's a no go. That leaves the Subscription object that's returned, but the goal here was to do away with that I thought.

Perhaps I don't understand the intent.

@domenic
Copy link
Member

domenic commented May 31, 2016

It's the returned promise whose state would be synchronously inspectable.

@trxcllnt
Copy link

@benjamingr Hundreds of code bases in RxJS isn't a large amount at all. In general, RxJS has not seen wide adoption until very recently - and that's arguably because it is being sold as part of the spec.

This isn't directed at @benjamingr, but I wanted to reply to the general sentiment this comment embodies that I've witnessed throughout this process.

Perhaps JavaScript isn't the appropriate environment in which to judge Rx's merits, especially since "spec-compliant" version has been largely developed with minimal input from the original authors (@headinthebox, @bartdesmet, @mattpodwysocki, etc.).

Here's the strongest case I can make for implementing the full Observable algebra (Observables, Observers, Schedulers, Disposables, etc.) as-is:

  1. The Observable algebra has been successfully implemented across a wide variety of languages and platforms, even platforms with drastically less support for functional programming styles than JavaScript. I know at least two large tech companies which have built, or are currently building, new languages with native Observables.
  2. Observable is the push-dual of Enumerable, which is itself supported by decades of research into Haskell's list monads. The algebra, while it may sometimes offend an individual's subjective aesthetic sensibilities, is internally logically consistent. Deviation from the duality will necessarily result in an algebra which can't describe as many problems (whether someone thinks that's an acceptable trade-off is an opinion they have a right to hold).

TC39 can certainly ignore the millions of dollars and dozens of man-years that companies like Microsoft, Google, and Netflix have poured into research, development, and battle-testing Rx, but considering this is the future of one of the most widely deployed programming languages in history, it would seem prudent to at least acknowledge the experience of those that have done it before, if not intensely study their findings.

@benjamingr
Copy link

@trxcllnt

First of all - I just want to point out I use a lot of Rx in my code, both in our backend in C# and in our frontend in JS. If I wasn't interested in the spec progressing and native observables in JS I wouldn't be here. I am not pointing out Rx isn't as popular in JS because I don't personally use it - I do so because it is the general sentiment I've been seeing. I probably used it less than anyone in this repo but more than most other people and we're heavily invested in Rx at TipRanks.

Second of all - I generally agree with your sentiment but remember: we're talking about the interface of the subscribe method of es-observable. This does not take away from the experience of people developing with reactive streams. This discussion is not about whether or not observables are useful and not a single person here disagrees wit that.

The API surface of Rx itself however - is a moving target in JavaScript.

Perhaps JavaScript isn't the appropriate environment in which to judge Rx's merits, especially since "spec-compliant" version has been largely developed with minimal input from the original authors

The original authors as well as authors of Rx-libraries from other languages have been consistently pinged. I've emailed some of them several times asking for involvement and they have been very helpful in helping es-observable figure out various points.

The Observable algebra has been successfully implemented across a wide variety of languages and platforms, even platforms with drastically less support for functional programming styles than JavaScript. I know at least two large tech companies which have built, or are currently building, new languages with native Observables.

Yet JS is the first mainstream language to gain native observable support - it's a lot easier to create a language with observables in than to modify an existing language to support them.

The fact a concept is popular does not mean it should be added to a language - the motivation to add observables to the language is for the platform and language APIs to use them, when asked about this the DOM people outlined requirements and we should do whatever we can to make observables work for them so they implement APIs on top of observables.

I'd like to see us be able to do input.clicks.subscribe(...), I want to be able to use Rx natively in web sockets, events and animations.

Observable is the push-dual of Enumerable

I think this is a part of the issue - JavaScript does not really have Iterator/Iterable types - there is no Iterable.prototype.map or anything similar and while I agree that Observable duals enumerable in some languages - there is simply no built in type for it to dual in JavaScript, you can't do Iterable.of Iterable.from Iterable.subscribe or any of that in JavaScript (yet).

Deviation from the duality will necessarily result in an algebra which can't describe as many problems (whether someone thinks that's an acceptable trade-off is an opinion they have a right to hold).

I think it's hard to talk about duality when the API of iterables is different across languages, if we were to go for a duality based design it would have to dual JavaScript's iterable which would create a strange interface. When this was discussed in this repo, all the Rx stakeholders were against it since it would imply:

Observable.of(...).subscribe(({done, value}) => {
  if(done) /* ... */
  // work with value
});

Duality is not sacred since a lot of iteration protocols are isomorphic with a trivial isomorphism. The API can change and the algebra will be as expressive (since well.. isomorphism).

TC39 can certainly ignore the millions of dollars and dozens of man-years that companies like Microsoft, Google, and Netflix have poured into research, development, and battle-testing Rx

Companies battle testing Rx and working on Rx can't ignore millions of dollars and dozens of man-years that companies like Google, Microsoft, Netflix, Mozilla and the rest of the TC members put into the language and the platform - it's a two way street.

I'm not happy about the outcome of the TC meeting any more than you are @trxcllnt, I want to make that much clear - but we need to work with the platform consumers of the proposal or focus on a non-native interop proposal between observable libraries.

@staltz
Copy link

staltz commented Jun 1, 2016

I'm not happy about the outcome of the TC meeting any more than you are @trxcllnt, I want to make that much clear - but we need to work with the platform consumers of the proposal or focus on a non-native interop proposal between observable libraries.

Bla bla, yes, we all know. We do. The real question is what in the vanilla Observable algebra represents an actual obstacle to (say) Chrome team or platform consumers? So far I've just heard aesthetic arguments like "the Web already has a primitive for signalling complete/error".

@benjamingr
Copy link

benjamingr commented Jun 1, 2016

Let me reiterate this @staltz - "the vanilla observable algebra" does not represent duality directly at all. There is no duality and there hasn't really ever been one. Moreover, unsubscription has never formally been dualized at all, especially not with subscriptions and disposables:

The role of disposables in the actual dualization has always been kind of handwavy (if you watch my talks, you will see that I grey out IDisposable and then pull a rabbit out of my hat to re-introduce it).
...
In some sense using .NET cancellation tokens and cancellation token sources https://msdn.microsoft.com/en-us/library/dd997289(v=vs.110).aspx in Rx would be cleaner than disposables. But that is another discussion

#14 (comment)

Whether or not duality is important is another issue.


Now - to answer your question directly:

  • If the web is settling on cancellation tokens for cancelling things - so should es-observable. The part about subscriptions and @Blesh's suggestion - I agree with.
  • Domenic describes in Remove forEach and add "subscribe" overload #97 (comment) several reasons, but foremost that the Chrome team is under the impression DOM APIs this proposal is targeting do not need complete/error APIs at all - let alone synchronous ones. What we need to do is come up with motivating examples for why those APIs are needed.

On another note, when you say things like "vanilla Observable algebra" it confuses me - I'm all game for discussing what that thing is but throwing it out there without formally defining well... an algebra over something (with well, a vector space and a map) is strange.

@staltz
Copy link

staltz commented Jun 1, 2016

Let me reiterate this @staltz - "the vanilla observable algebra" does not represent duality directly at all. There is no duality and there hasn't really ever been one. Moreover, unsubscription has never formally been dualized at all, especially not with subscriptions and disposables:

What? I didn't even mention duality. Paul did.

If the web is settling on cancellation tokens for cancelling things - so should es-observable.

Not too fast. The web is not settling on anything. Domenic is settling on cancellation tokens. So far, cancellation tokens have been really badly received by the community and by experts, so there is definitely no settling going on, specially because it's a stage 1 proposal, not based on community practices to begin with. Also, it's not for "cancelling things". It's for cancelling Promises. Observables are different beasts than Promises, they can well have their own cancellation mechanisms.

Domenic describes in #97 (comment) several reasons, but foremost that the Chrome team is under the impression DOM APIs this proposal is targeting do not need complete/error APIs at all - let alone synchronous ones. What we need to do is come up with motivating examples for why those APIs are needed.

Sure, DOM APIs like clicks are always infinite Observables of zero or many click events, but the main selling point for Observables is composability, and there are plenty of motivating examples related to that. In the example I gave before, repeated below, the top Observable is entirely async, it could be very well representing a click Observable from the DOM for instance. It is the composition with other Observables (in this case, a sync Observable of true, false, and 'complete') that makes it useful.

--a-----b----c----d----
  flatMap(x => isVowel(x) ? Observable.of(true, false) : Observable.of(x.toUpperCase()))
--(TF)--B----C----D----

And the composability features depend on the solid foundation that:

  • Observables may emit next or error or complete, synchronously or asynchronously
  • Follow the contract (in regex) next*(error|complete)?
  • Each subscribe spawns a new and independent subscription chain upstream
  • Each subscribe returns a Disposable, which can be grouped with other Disposables, and has one key method dispose() or unsubscribe()

Without composability, there is no point in Observables. No point at all, because you could just have an addEventListener-like API with three callbacks as args.

And by the way, the list above could well be an informal spec for the "algebra".

@briancavalier
Copy link

@staltz I let this one slide, just now answering.

Was it for performance, or to reuse the Web's standard for signaling success/failure?

The latter. Also, most.js Streams never produce an event/end/error before a consuming operator, e.g. forEach, returns (tho after it returns, propagation is allowed to be fully synchronous). The analog between that multi-valued case and single-valued case felt strong. Also, the analog between reducing a synchronous collection to a synchronous non-promise value, and reducing an asynchronous stream to an asynchronous single-valued promise felt strong.

@benjamingr
Copy link

benjamingr commented Jun 1, 2016

The web is not settling on anything. Domenic is settling on cancellation tokens.

@zenparsing @jhusain can you please elaborate on the consensus that was reached regarding cancellation tokens in the meetings?

Also, it's not for "cancelling things". It's for cancelling Promises. Observables are different beasts than Promises, they can well have their own cancellation mechanisms.

No, the TC discussed and decided against promise cancellation in the meeting (see tc39/proposal-cancelable-promises#10 (comment)) - I'm not happy about that either :)

Promises are obviously very different from observables - they have very different semantics. What the TC decided from what I understand is to use tokens for cancellation of results of asynchronous functions. When other TC participants respond we'll have more data about the decision.

Each subscribe returns a Disposable, which can be grouped with other Disposables, and has one key method dispose() or unsubscribe()

I'm interested in what @headinthebox was discussing when he said cancellation tokens could have made Rx cleaner in retrospect. Either way - calling subscriptions cancellation tokens like @Blesh suggested doesn't sound too harmful to me.

Sure, DOM APIs like clicks are always infinite Observables of zero or many click events, but the main selling point for Observables is composability.

I agree, and we need to show how that composability is hurt if .subscribe returns a promise and how writing code becomes not just less ergonomic but also less expressive.

I would appreciate it if from the height of your experience you would write about that (and not "Bla bla") :)

p.s.

Follow the contract (in regex) next*(error|complete)?

that's pretty creative :D

And by the way, the list above could well be an informal spec for the "algebra".

I challenge you to make one and formalize it - I would be interested in the results.

@benlesh
Copy link

benlesh commented Jun 1, 2016

One thing that hasn't been mentioned is that if success and error are signaled by a promise, those two pieces of Observable are now always multicast. That will fundamentally change the type, to what end I'm unsure. It seems like it would be fine, but it also seems like an unnecessary change.

@staltz
Copy link

staltz commented Jun 1, 2016

@Blesh as long as subscribe returns a new Promise every time it's called, that's a harmless multicasting, since it won't usually (or ever?) have multiple consumers. That's the most harmless part of the proposed change, but anyway good to mention. I don't have the creativity to imagine cases where the code that subscribes to the observable does not also do .then/.catch on the returned promise, but instead passes that promise to some other module.

@trxcllnt
Copy link

trxcllnt commented Jun 1, 2016

tl;dr: Changes to the API or behavior aren't necessarily isomorphic, such as enforcing async subscriptions, emissions, errors, or completions. Changing those things will change the expressiveness of the algebra.

@jhusain The narrative presented to the committee was "synchronous composition, asynchronous consumption." This is why the "from" and "of" methods were switched to be synchronous methods, and "forEach" was changed to schedule subscription.

TC39's latest thoughts on sync vs. async subscribe/forEach are unclear to me. Are subscribe and forEach still distinct, or are they aliases of one another? If they're aliased and subscriptions are scheduled asynchronously, groupBy is still broken. The subscription to the inner group needs to be synchronous, otherwise it will miss the first n emissions (where n is the number of values synchronously emitted by the source that should be routed to the group).

We've established why synchronous composition is needed to correctly implement certain combinators (groupBy). However I don't think it has yet been established that scheduling completion or error notifications makes writing any of the existing combinators impossible. I think this should be our focus.

I'm sorry I didn't think about this last week, but scheduling errors or completion breaks synchronous Observable composition. Subscribing to Obs.merge(Obs.throw('bad'), Obs.range(0, 10)) should subscribe to the first source (throw('bad')), which synchronously emits the error value, disposes of the merge subscription, and never subscribes to the second source (range(0, 10)). I can't think of a way to do partial synchronous emission that doesn't break composition this way.

groupByUntil on a synchronous source would also be affected. This example (using Rx5 groupBy with a duration selector) will never complete the inner groups, because the synchronous source emission blocks the asynchronous completion:

Observable.of(1, 2, 2, 2, 3)
  .repeat()
  .groupBy((x) => x % 2 === 0, null, (group) => group.take(3))
  .mergeAll()

@domenic The web already has one data structure for signaling success/failure; what it could benefit from is one for repeated invocation, i.e. the "next" part of observables. Any DOM observables, per the document in this repo, would not even use the success/error parts.

I understand the desire for re-use, but Promises' enforced asynchrony seems to prohibit using them for this purpose. While I find Promises to be a worthwhile addition to the language, Promises can be expressed as a subset of the Observable algebra, but not the other way around :/

As such, it's a requirement of the Chrome team that observables layer cleanly with the platform's existing primitives for success/error, promises, if they are to become part of the JavaScript language and not simply a library.

Has the Chrome team considered an Observable -> Promise interop/conversion? An Observable can be made to behave like a Promise at the exit (or subscription) point, yet maintain the compositional Observable semantics before then.

@benjamingr First of all - I just want to point out I use a lot of Rx in my code, both in our backend in C# and in our frontend in JS. If I wasn't interested in the spec progressing and native observables in JS I wouldn't be here. I am not pointing out Rx isn't as popular in JS because I don't personally use it - I do so because it is the general sentiment I've been seeing. I probably used it less than anyone in this repo but more than most other people and we're heavily invested in Rx at TipRanks.

I apologize if my comment sounded like I was singling you out, that was not my intention. You've contributed valuable insights in good faith to all the discussions I've been involved in, whether we agreed or not. I'm also aware that you're probably already familiar with everything I've said below, but I've responded anyway for the benefit of anybody who isn't.

Yet JS is the first mainstream language to gain native observable support - it's a lot easier to create a language with observables in than to modify an existing language to support them.

I'm not sure whether to regard LINQ, async/await in C# as native language support, but I understand what you're getting at.

The fact a concept is popular does not mean it should be added to a language

100% agreed 👍

the motivation to add observables to the language is for the platform and language APIs to use them, when asked about this the DOM people outlined requirements and we should do whatever we can to make observables work for them so they implement APIs on top of observables.

I would like to achieve this as well. However, I'm not aware of any problems that have been presented which clearly suggest Observable is deficient. I have heard arguments that Observable is over-kill, that it has too many methods, that it doesn't comply with the "language aesthetic," or that functional programming will never be mainstream. The mutability of these arguments frustrates me.

There are a good many problems that Observables don't solve. I would understand if the pushback was from concern about async disposal, or back pressure. These are two problems which the JS Observable spec doesn't presently address, but prior art suggests can be added to the Observable type without altering the underlying semantics.

Companies battle testing Rx and working on Rx can't ignore millions of dollars and dozens of man-years that companies like Google, Microsoft, Netflix, Mozilla and the rest of the TC members put into the language and the platform - it's a two way street.

Let me state that I'm unequivocally sympathetic to the needs and desires of the parties invested in the web platform. I'm simply trying to express the idea that no amount of consensus can overrule a set of immutable mathematical principles. The Observable just can't do certain things if some of the changes that have been proposed are dictated. Again, whether that matters or not is up to each person to decide for themselves.

Domenic describes in #97 (comment) several reasons, but foremost that the Chrome team is under the impression DOM APIs this proposal is targeting do not need complete/error APIs at all - let alone synchronous ones.

I understand their position, and it's defensible. The traditional GoF Observer pattern would entirely satisfy their requirement, as it has in nearly every other UI environment for over two decades. The great discovery @headinthebox et. al. made a few years ago is that with just two additional messages, error or complete, the Observer pattern can be extended to solve new classes of problems in asynchronous programming.

I think this is a part of the issue - JavaScript does not really have Iterator/Iterable types - there is no Iterable.prototype.map or anything similar and while I agree that Observable duals enumerable in some languages - there is simply no built in type for it to dual in JavaScript, you can't do Iterable.of Iterable.from Iterable.subscribe or any of that in JavaScript (yet).
I think it's hard to talk about duality when the API of iterables is different across languages, if we were to go for a duality based design it would have to dual JavaScript's iterable which would create a strange interface.
Duality is not sacred since a lot of iteration protocols are isomorphic with a trivial isomorphism.

I couldn't agree more. I wasn't advocating for duality as a holy grail, especially not duality with JavaScript's Iterator/Iterable. I brought up duality with Enumerable because Enumerable, as it exists in C# and elsewhere, is a well researched, thoroughly specified, and foundational concept in many mainstream functional programming languages.

By achieving duality, Observable extends that research effort to apply to data from the future too. Duality ensures you can perform identical calculations whether the input data is from the past, future, or even a mix of both. Maybe I'm alone in thinking this, but the implications of that guarantee are damn exciting. I feel like a goddamn Time Wizard every day I go into work. I would be very disappointed if we choose aesthetically pleasing but incompatible semantics at the expense of that truly fantastic capability.

@jhusain
Copy link
Collaborator

jhusain commented Jun 1, 2016

No consensus was reached with regard to replacing subscriptions with cancellation tokens. However we did discuss whether it was possible.

The issue in my mind is that subscription guarantees that no more notification will be sent to the observer, whereas canceling a cancellation token makes no sense guarantee with regard to whether a promise will be resolved.

Observable can enforce additional guarantees not implied by the cancellation token. They can intercept the cancel method and ensure that observers are never notified once it has been invoked. Maintaining this invariant would likely involve additional runtime checks, but should be possible.

JH

On Jun 1, 2016, at 5:06 PM, Benjamin Gruenbaum notifications@github.com wrote:

The web is not settling on anything. Domenic is settling on cancellation tokens.

@zenparsing @jhusain can you please elaborate on the consensus that was reached regarding cancellation tokens in the meetings?

Also, it's not for "cancelling things". It's for cancelling Promises. Observables are different beasts than Promises, they can well have their own cancellation mechanisms.

No, the TC discussed and decided against promise cancellation in the meeting (see tc39/proposal-cancelable-promises#10 (comment)) - I'm not happy about that either :)

Promises are obviously very different from observables - they have very different semantics. What the TC decided from what I understand is to use tokens for cancellation of results of asynchronous functions. When other TC participants respond we'll have more data about the decision.

Each subscribe returns a Disposable, which can be grouped with other Disposables, and has one key method dispose() or unsubscribe()

I'm interested in what @headinthebox was discussing when he said cancellation tokens could have made Rx cleaner in retrospect. Either way - calling subscriptions cancellation tokens like @Blesh suggested doesn't sound too harmful to me.

Sure, DOM APIs like clicks are always infinite Observables of zero or many click events, but the main selling point for Observables is composability.

I agree, and we need to show how that composability is hurt if .subscribe returns a promise and how writing code becomes not just less ergonomic but also less expressive.

I would appreciate it if from the height of your experience you would write about that (and not "Bla bla") :)

p.s.

Follow the contract (in regex) next*(error|complete)?
that's pretty creative :D

And by the way, the list above could well be an informal spec for the "algebra".

I challenge you to make one and formalize it - I would be interested in the results.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

@benjamingr
Copy link

Would it be possible to schedule a chat or hangouts session between interested participants so we find ourselves in a situation such that in the next TC meeting the proposal is ready to progress to stage two?

Namely, I think an agenda could look something like:

  • Discussion of the importance (or lack of) of synchronous dispatch of complete and error events.
  • Discussion of how cancellation tokens would work with observables if at all.
  • Discussion of how different APIs would look with different design suggestions.

Is this something that could happen?

@mattpodwysocki
Copy link
Collaborator

@jhusain @zenparsing If it will help I will be attending the next TC39 meetings as they are in Redmond anyhow. I can bring Barr de Smet with me as we designed RxJS together and he has a lot of insight with async subscription and disposal. Else I'm willing to be in any hangout on the matter.

@mattpodwysocki
Copy link
Collaborator

@benjamingr I don't see why not. More than willing to help

@alex-wilmer
Copy link

@domenic Would it be possible to go into more detail here about what makes the RxJS model incompatible / conflicting with the existing spec? Would things break? Is it unnecessary bloat?

@jhusain
Copy link
Collaborator

jhusain commented Jun 3, 2016

I'm certainly open to this. It would be ideal if @domenic could attend. The committee expressed interest in us making a joint presentation. The committee wants to make sure that we cover the full spectrum of use cases with the types in proposed for language inclusion. Given the chrome teams concerns, it would be good if they were represented.

JH

On Jun 2, 2016, at 1:10 PM, Benjamin Gruenbaum notifications@github.com wrote:

Would it be possible to schedule a chat or hangouts session between interested participants so we find ourselves in a situation such that in the next TC meeting the proposal is ready to progress to stage two?

Namely, I think an agenda could look something like:

Discussion of the importance (or lack of) of synchronous dispatch of complete and error events.
Discussion of how cancellation tokens would work with observables if at all.
Discussion of how different APIs would look with different design suggestions.
Is this something that could happen?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

@zenparsing
Copy link
Member Author

I'm going to pull this PR in, as I think it represents the ideal form of the Rx-style Observable API.

As far as the promise-based observable design goes, apologies for not getting involved in the discussion sooner. I originally experimented with a promise-based observable design a few months ago here:

https://github.com/zenparsing/zen-observable/tree/master/white-board/cancel-token

My interest in the design fizzled out for a couple of reasons:

  • Responding to cancellation and doing to the right thing when you can only listen for a promise (i.e. cancelToken.promise) turns out to be a little tricky.
  • The traditional observable API is more ergonomic for typical event listening use cases.
  • Even with the promise-based design, you still have to deal with the synchronous dispatch issue. I've come to the conclusion that the best approach is to embrace synchronous dispatch rather than fight it.
  • I felt there would be little to no interest in the design from the Rx community.

My hunch is that we'll eventually come back around to this more traditional API, in part because cancel tokens seem to want a synchronous listener registration API.

let subscription = cancelToken.subscribe(() => {
  // Do cleanup and stuff
});
subscription.unsubscribe();

I'm happy to further explore the promise-based API though.

@zenparsing zenparsing merged commit ef98438 into master Jun 13, 2016
@zenparsing zenparsing deleted the remove-for-each branch June 13, 2016 02:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet