-
Notifications
You must be signed in to change notification settings - Fork 110
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
Compare and contrast GToR Stream/Observable vs Rx Observable #6
Comments
I think Rx conflates streams and observables in a lot of ways and confuses more than illuminates in that respect. For example, Jafar does point out that async generators can use await yield to pass back pressure. For the purposes of GtoR, observables and signals are strictly for lossy (a word I need to use in this article) time series data and pressure is not involved. |
Yeah, I kind of got that from your approach, but you never really came out and said "the slides are wrong"; in fact you said something more like "the slides are right." |
Yes, that is my mistake. Parts seem right to me other parts I don’t understand well enough to judge but don’t match what I’ve described here. |
Is there a link to Jafar's slides? |
Indeed. Copied here… https://docs.google.com/file/d/0B4PVbLpUIdzoMDR5dWstRllXblU/edit On Fri, Aug 15, 2014 at 5:41 AM, zenparsing [email protected]
|
I am still absorbing these slides and the Rx mentality. I am certain at this point that the interface Rx describes for observables is what I describe as an readable output stream or promise iterator. Regardless, I like, no…love, the idea of an Hypothesis: The semantics of Rx style observables are multi-faceted, capturing both lossy time series data semantics and lossless pressure semantics depending on some factor I am yet to discover. Hypothesis: Rx appears to have per-iteration error handling built-in whereas GToR proposes orthogonal idioms for per-iteration and per-stream error handling. rxObservable.subscribe(function () {
console.log("rxObservable produced an iteration normally");
}, function () {
console.log("rxObservable was unable to produce a value for this iteration");
});
gtorStream.forEach(function (n) {
return blah().then(function (value) {
console.log("iteration produced normally");
}, function (error) {
console.log("failed to produce an iteration");
});
// the resolution of either handler is the iteration,
// and if an error escapes, it will terminate the stream
// prematurely.
})
.then(function (value) {
console.log("stream terminated normally");
}, function (error) {
console.log("stream terminated prematurely");
}); |
I am still absorbing the slides as well. It will probably take me another week of consideration, but at this point I'm somewhat disappointed that I don't find a clear argument against what seems like the obvious path: that an async generator returns an iterable of Promises. More explicitly:
Can observables not be constructed from iterables of Promises? I haven't tried to prove it yet, but it seems like |
@zenparsing can you specify what you mean by iterable of promises? Which of the following does
Near as I can tell, what Jafar describes as |
Right - that was a confusing way to put it. I meant that But I'm a little unclear on this: what is |
I think I can answer myself: In that case |
Yes, though be aware that it is a term I fabricated and hasn’t by any means caught on. So I believe that we are all in agreement of the type signature of an async generator, including Jafar. |
Yes, although Jafar makes the async (EDIT: generator) function, when called, return an |
Looking over the slides again, I believe Jafar’s async generators do return async iterators, not async iterables. The JavaScript I will have to write an article just going over this slide by slide and pinpointing any specific disagreement. A lot of this looks algebraically equivalent to what I’m proposing. He is proposing that He says that Object.observe is an async generator. This is more the true sense of an observable, because it disregards back pressure, but the interface is a subset for all intents and purposes, so I can’t say we disagree. |
And this analysis has made it much more clear to me that |
My design inclinations are in line with yours. I'm curious why Jafar chose to not expose a |
My take on things was that his observables are fundamentally push: once you call the async generator function, its behavior is out of your control entirely, and you get notified if/when it wants you to, via the subscription callback. This seems in conflict with the normal generator next() model, where the consumer, via calling next, gets some measure of control over the body of the function. So I think that's why he doesn't have a next(). I also want to second @zenparsing that I don't see the iteration object exposed anywhere in his design, so the type signature is not in agreement with the |
Thanks @domenic - that makes sense when looking at the slides. It seems an odd choice though - I don't think it's difficult to come up with scenarios where you want to write an async generator where the consumer is in control. I tried writing a https://gist.github.com/zenparsing/26b200543bb8ae0ca4df One thing that jumps out at me that I didn't really consider before is that there is no way to access the value of the first call to If so, then it makes writing these streams awkward because you have to artificially "pump" the iterator with a useless call to For example, if your async generator is writing buffers to a file, then there's no way to capture the buffer that is passed to the first call to @domenic @kriskowal what do you think? |
This appears to be an issue for Jafar's design as well. The I've posed the question on es-discuss: http://esdiscuss.org/topic/that-first-next-argument |
When composing an async function and a generator function, we have two precedents. A generator function does not resume until we first call |
I think I disagree here. Consider this. If an async function does not have an async function af() {
console.log("inside call");
}
console.log("before call");
af();
console.log("after call");
/*
> "before call"
> "inside call"
> "after call"
*/ By symmetry, I would expect that an async generator without async function *ag() {
console.log("1");
yield 1;
console.log("2");
}
console.log("before call");
let iter = ag();
console.log("after call");
console.log("before next");
iter.next();
console.log("after next");
// ...etc
/*
> "before call"
> "after call"
> "before next"
> "1"
> "after next"
...etc
*/ |
I find @zenparsing's argument compelling. |
Yes, it’s good and I buy it. |
I think I finally have a handle on the crux of the difference between Observables and Async Iterators. Iterators transmit data from callee to caller, down the call stack. Observables transmit data from caller to callee, up the call stack. Not coincidentally, event-subscriber systems also transmit data up the stack. I think that explains why the slides focus more heavily on representing event streams (as opposed to data processing streams). Does that sound right? |
Re-reading this thread ... it's a goldmine. This came up at the last TC39 meeting again. The only compelling argument given for async generators vending observables instead of async iterators was efficiency reasons. I.e., allocating a That seems possible, but again, more relevant to event streams than to data streams. The other argument was somewhat of a pragmatic one. Namely, that observables (and their focus on events) are more useful as a pattern in general than async iterators. And thus, if we're willing to bless something with syntax, it should be observables. That way, you can use I still think observables are a bad match for Indeed, a theoretically sound design seems to me something like:
If we were to bless them with syntax I'd do something like
Of course this is a pretty ridiculous expansion in complexity for unclear use cases: pragmatically forEachables are pretty silly, and it's questionable whether we have room for async iterables and observables. Still, it strengthens my belief that |
Saw that this was briefly discussed at the tail end of the meeting notes. Glad that this is being discussed. I do think there is grounds for both push and pull abstractions at this layer. This reminds me of the layers of arithmetic operators. At layer one you have the dichotomy of plus and minus, then at layer two you have multiplication and division which would be symmetric alone, but then you get the hangers-on of modulo and remainder. At the third layer you have exponentiation. The analogue of subtraction at this layer is the radical, but the logarithm is a dual in another sense. Seems that each time you move up a layer, more variations reveal themselves. |
Yeah, as you move up in abstraction, you lose simplifying symmetries, and so you gain more complexity when you invert or interact with the now-fractured operators. Handling that sort of thing well requires a very well-thought-out system for expressing the new operations; just taking the first manifestations and throwing ad-hoc syntax at them won't do very well unless you get lucky. We need to some in-depth study on this, see how many of the axises are relevant enough to be worth addressing, and find some unifying patterns that make them all make sense and carve up the idea-space in a way that's reasonably easy to understand and works well in syntax. |
@tabatkins That is the charter for this living document. |
Yeah, I know. ^_^ Just saying, for Domenic's benefit. |
🧟 🧙♂️ RESURRECTED (sorry) @domenic I've thought about this a lot over the years, and I've always thought this sort of thing has some merit, but it would require something different than function^ pushThings() {
push 1;
push 2;
push 3;
}
const values = pushThings(); // Observable<number>
values.forEach(console.log); // logs 1, then 2, then 3 synchronously (could be scheduled)
// or this is equivalent. (which couldn't be scheduled?)
for (const value on values) {
console.log(value);
} Where an async function^ would be more like this: async function^ pushAsyncThings() {
for (let n = 0; n < 100; n++) {
await sleep(1000);
push n;
}
}
const values = pushAsyncThings();
for await (const value on values) {
console.log(value);
} The interesting thing this could provide, though, is the ability, along with the WHATWG proposed addition to async function^ pushedPricesFor(symbol, signal) {
const socket = new WebSocket('wss://priceinfo/socket/endpoint');
// Start a subscription to errors that causes
// it to throw. Since `push` (or `push*`) doesn't
// block execution, maybe this just runs until the
// code block is complete
push* socket.on('error').map((e) => {
throw e;
})
// Wait for our socket to open
await socket.on('open').first();
// Then send our symbol to start getting streaming data
socket.send(symbol);
// Yield our data from the backend
push* socket.on('message')
.map(e => JSON.parse(e.data))
.takeUntil(signal.on('abort'));
const closeEvent = await socket.on('close').first();
if (!closeEvent.wasClean) {
throw new Error('Socket closed dirty.') // Network error maybe?
}
} |
The section on async generators seems confused about what Jafar's slides propose. It proposes Rx-style observables (your signals, I believe); it does not propose anything like the async iterators in GTOR.
The text was updated successfully, but these errors were encountered: