DEV Community

loading...

Discussion on: Observables, Reactive Programming, and Regret

Collapse
richytong profile image
Richard Tong

Thank you for taking the time to reflect on RxJS. The truth is, I probably wouldn't be creating a library right now if RxJS didn't exist because RxJS set such a huge precedent for the functional (reactive) programming paradigm. Sometimes you just gotta try new stuff to see what works. I respect that mentality.

RxJS is a library of functions built around observables, not the other way around.

This is my biggest gripe with RxJS. If in a separate universe this were not the case, I'm sure I would be an active RxJS advocate right now. I started rubico because I saw the value of the functional programming paradigm but could not find any existing solutions I was happy with. The primary reason I was not satisfied with RxJS was in fact the Observable; if RxJS had been built around Promises instead of Observables, I would have reached for it in a heartbeat. That is really the only difference between our two libraries; RxJS is to Observables as rubico is to Promises.

Why do I oppose Observables so much?

It's because they, by themselves, make life hard. You can't just assert.deepEqual the value of an Observable. Instead you have to switch your mind to virtual time and use mock Observables. Promises have way better interop with the regular types; it's a one-to-one mapping. Promise of string? Cool. Promise of Array? Also cool. Observables are inherently many-to-one. This is why it's pretty common in practice to use RxJS operators mergeMap or concatMap; you have to deal with Observables of Observables. flatMap is a powerful concept, but in the case of Observable, it's a necessity.

The Observable is the dual of the Iterable, but that by itself should not give it any more of a case to make it into the spec. Shouldn't we as library authors create things that make developers' lives easy? All I see is Observables making life hard, and I can't get behind that.

Collapse
louy2 profile image
Yufan Lou

You can't just assert.deepEqual on Observable not because it is many-to-one, but because it is lazy, while Promise is strict. When you compare two Promises you are comparing their state and result cache, whereas when you compare two Observables you are comparing all their callbacks, which makes less sense.

Laziness is what gives the cancellation control. Without it you need the cancellation token workaround. C# uses the same workaround too. Between the guarantees and strictness, we each pick our trade-off.

The Observable is the dual of the Iterable, but that by itself should not give it any more of a case to make it into the spec.

The dual of an existing tool is almost always a equally powerful tool, as borne out by mathematics many times over. I agree that Observable needs more polish to improve developer experience. But I am more disappointed by a spec which chooses to break duality without informed ground. It was a chance to future-proof the Promise API and the spec threw it.

flatMap is a powerful concept, but in the case of Observable, it's a necessity.

FlatMap (monadic bind) is a necessity for programming at all. It is a mathematical model for sequential context. Historically programs focus on one such context: the single-thread procedure. The source code line sequence represents that. In Haskell, any such context can use the do notation and avoid calling flatMap directly. In JS, async/await does that for Promise (of which then is flatMap anyway), and there is no reason it can't do so for Observable apart from that Promise is in the spec but Observable is not. Arguing that flatMap is necessary for Observable but not for Promise is misunderstanding Promise. Arguing Promise is more worthy to be in the spec for that reason is circular logic.

Collapse
richytong profile image
Richard Tong

You can't just assert.deepEqual on Observable not because it is many-to-one, but because it is lazy, while Promise is strict.

Of course you wouldn't just assert.deepEqual two Observables or two Promises, it's the step between getting something assert.deepEqual-able from Observables that is so much harder than from Promises. With Observables, you have to mock them in virtual time or fully consume them to some arbitrary type (losing precision) to test their behavior. With Promises, there is no time parameter; it's a one-to-one mapping from the Promise to its associated value.

Laziness is what gives the cancellation control.

The Observable's implicit higher order as a type is what gives both laziness and cancellation control. Observables live at a level of abstraction that makes cancellation convenient. I would know because my library lives at a similar level of abstraction. It even implements Promise cancellation on async iterable transformations. The proposal for promise cancellation was withdrawn because

TC39 has decided to investigate a cancellation mechanism in the core library

Maybe they were talking about cancel tokens here? Either way, proponents for Observables rejoiced when this proposal was withdrawn, yet happily continue to cancel Observables. Cancellation in the observable proposal is pretty up in the air, by the way, and is far from standardized. I don't understand why you guys keep waving it around.

The dual of an existing tool is almost always a equally powerful tool, as borne out by mathematics many times over.

Are you talking about Observables here? And implying it should thusly make it into the spec? Does that mean all things dual by mathematics should be standardized? Also, could you clarify what you mean when you say you are "more disappointed by a spec which chooses to break duality without informed ground?" Right now I'm interpreting it as you think Observables should make it into the spec by mathematical duality.

FlatMap (monadic bind) is a necessity for programming at all.

It's really not. Have you tried working with built-in types? I understand if you're working with effectful types all the time that you might see the world this way, but in practice I hardly need flatMap at all.

In JS, async/await does that for Promise (of which then is flatMap anyway)

All you're telling me here is you haven't grokked Promises. .then has its own semantics and is absolutely not a flatMap for Promises. This example with a .flatMap polyfill on Promise illustrates their difference. The following two expressions are equivalent.

Promise.resolve(1).flatMap(x => Promise.resolve(x + 1)) // > Promise { 2 }
// flatMap expects a Promise in the return

Promise.resolve(1).then(x => Promise.resolve(
  Promise.resolve(Promise.resolve(x + 1)),
)) // > Promise { 2 }
// with .then, the Promises are flattened automatically to the nested value

Arguing that flatMap is necessary for Observable but not for Promise is misunderstanding Promise.

Arguing flatMap is necessary for Promise is misunderstanding Promise.

Arguing Promise is more worthy to be in the spec for that reason is circular logic.

Promise is already in the spec.

Thread Thread
louy2 profile image
Yufan Lou

could you clarify what you mean when you say you are "more disappointed by a spec which chooses to break duality without informed ground?"

I am no proponent of Observable, nor have I interacted with them. I feel the same as you about its verbosity, and I appreciate your rubico library. In the end everyone mostly have moved on to async functions and async iterators, which are at where I wished the level of abstraction Promise spec was. As references, Rust Future is conceptually Future { poll :: () -> Pending | val }, and Haskell Async is conceptually Async { _asyncWait :: () -> SomeException | a }, both of which have their own async/await. Promise is essentially the Pending | val part.

.then has its own semantics and is absolutely not a flatMap for Promises

.then has its own semantics including a flatMap for Promises. The minor mismatches are just a few joins or a pure away. The important part is the concept of sequence.

I understand if you're working with effectful types all the time that you might see the world this way, but in practice I hardly need flatMap at all.

Who are not? I guess we differ about what is effectful. State and error handling are central to programming and are effectful in my vernacular. You just don't need to explicitly write out the flatMap because the language takes care of it.

Thread Thread
richytong profile image
Richard Tong

State and error handling are central to programming and are effectful in my vernacular. You just don't need to explicitly write out the flatMap because the language takes care of it.

Actually I see your point here. I'm becoming more aware that basically everything is an effect, especially when it comes to designing languages. I see how the language can take care of stuff like this; in fact rubico provides a language that takes care of Promises in this way.

The minor mismatches are just a few joins or a pure away. The important part is the concept of sequence.

This thought crossed my mind when I was writing out that example. Basically Promise .then does a whole bunch of flatMaps arbitrarily, if required. I'll chalk it up to a difference of what is effectful.