The rx fallacy is to make you believe that programming at the most general abstract level is the best. It’s just wrong. An array is not a stream, an observable that can fire at any time isn’t the same as a predefined list of things.
Whenever you abstract a data structure, you loose some meaning, some peculiarities that helps you understand what you’re doing.
Rx should be used by framework or low level libraries developers, to factor code. But it should be almost invisible to the api user.
Programming is arguably the process of abstracting data (and functions which are also data). Abstraction does hide information, but it does so to expose the essentials. Good abstraction should lead to DRY code.
Streams are a form of abstraction that allow for __pure__ functional "event" patterns. OOP world like JS events and event handlers, allow mutations and have no problem sending messages with objects to process. The problem with this from a pure functional paradigm is that it isn't very composable. Two functions called one after another may not always compute the same thing if they each respond to random events whenever. The solution is to create a "stream" abstraction, and to have specific data types handling different types of streams. By thinking of events as streams, you now have a "neutral" signal that you tap into, and you just wait until you have a message. Without explaining and going into details, this allows for composable, pure function 'events'.
To summarize, streams are taking mutable event patterns into the world of pure (immutable) functional "event" patterns. And this is good if you already are convinced mutations are bad (different topic).
Pure functional only takes you so far. If you’re using transient servers to process http queries that has no local state, then the frp abstractions are probably a good fit.
When you’re dealing with an app or a single page web app, you’re spending the whole user session mutating one big state (represented on the UI). Every events are going to apply some diff over that big state, and so what you have in practice is a stream of effects.
FRP is purely functional. Side effects can be managed with pure functions using Monads, and Streams are also Monads because of the interface they implement.
>The rx fallacy is to make you believe that programming at the most general abstract level is the best. It’s just wrong.
Citation needed.
>Whenever you abstract a data structure, you loose some meaning, some peculiarities that helps you understand what you’re doing.
That's obviously not true.
The whole idea is to abstract over peculiarities that are not needed (implementation details for example).
So, you "loose some meaning, some peculiarities that helps you understand what you’re doing" only some times (when you abstract badly) and not "whenever you abstract a data structure".
>Rx should be used by framework or low level libraries developers, to factor code. But it should be almost invisible to the api user.
That makes even less sense - having abstraction as the hidden lower level. The whole point of abstraction is giving a higher level interface.
Abstraction is, by definition, reducing different things to a shared subset of their attributes.
Eg : the concept of "Human" is an abstraction over all the different attributes of all the individuals, to reduce them to the ones they all share. If i say i met a "human being", you can't tell if it's a "he" or a "she", if he has dark of blond hair, etc. What you can tell, is that it has a heart and a brain, stand on at most two legs, and has at most two arms. That could be all you need to deduce a few things, but by saying so, i kept some details hidden from you.
Now, if we get back to rx, its whole purpose is to work with the concept of "streams of events". Noting that many data structure and patterns (such as iterators over arrays, or event source observation) can be thought a specific kind of even streams.
This lets you define stream-combination operations, that work on all those structure. Which may, sometimes, be convenient.
The problem is when people start telling you that the generic tools around Rx Stream (producer, consumer, publish subject, etc) are the best tools to model the behavior of all your application, because "everything is in fact one big giant stream of events".
And then you end up with code monstruosities, because you realize that it's more complicated than that. Maintaining a UI state and mutating it becomes a nightmare, you start wrapping every native structures (such as arrays) into Observers just to perform the equivalent of a join, etc.
Either the stdlib and frameworks are "reactive", and then using rx pattern would be invisible, or you're going to have a horrible code.
In some way, it's exactly like generics, or base superclass : you define them in the stdlib, for example to share code between Sets and Arrays, but what the end user is manipulating 99% of the time isn't "Iterable". It's Arrays and Sets.
The problem is that Rx isn’t about “streams of events”. The main interface, IObservable, is entirely about _subscribing_ to “streams of events”.
It’s the awful brittleness of handling those subscriptions that makes it so hard to program with.
Compare that to something like signals in Elm (originally, it has changed now) while it makes some things Rx allow you to do impossible reports from users are that once it type checks “it just works”
You don’t need citations in a comment thread discussion, the same way I don’t ask my friends over beers for citations when they imply the Maple Leafs is a hockey team. We all know that is false.
- 99.9999% of examples and tutorials on the internet fit one page of code. It's enough to look at something more complex, and you get immediately lost, because:
- Rx is very good at modelling streams that go in one direction, from A to B. From B to C. The to D etc. The moment you need the results of computation in C back in A... Oh, yikes. You start having Subjects everywhere (or BehaviourSubjects? Or ReplaySubjects? Or AsyncSubjects?) which may and will (if you are just starting learning these things) become extremely messy
- Rx may be easily viewed as async programming. Because your transformations happen at some unspecified non-deterministic point in time (and a lot of operations are async). So, debugging is hell. RxJS and (IMO) RxJava have tried to improve this, but it's still a mess. Properly structuring your code and streams so that you understand why and when a certain something happens? No one teaches you that. And you need that because your `.map(transform)` has no idea when and who invoked it. Etc.
- it doesn't help that Rx has cold and hot observables that everyone gets burned by. And I mean everyone. Oversubscription? Undersubscription? Events not firing? Events firing multiple times? These all happen. And some stream libraries go as far as not having cold observables at all. Good article about that: https://staltz.com/cold-and-hot-callbacks.html
- My favourite thing: everyone tells you how easy and simple Rx is: it's just observables. In his book on RxJava the creator of RxJava says that it took him several months to understand Rx. While being tutored by one of the creators of ReactiveX. It's "easy and simple" in the same sense as "Haskell is easy and simple" or "rocket science is simple and easy" or <any branch of human knowledge> is simple and easy once you know and understand it.
All in all, streams of things are a good idea. Splitting your work into small isolated side-effect free chunks of work is a good idea. With Rx though, unless you are bright yourself or you have someone to mentor you, Rx is an unassailable bastion.
"- My favourite thing: everyone tells you how easy and simple Rx is: it's just observables. In his book on RxJava the creator of RxJava says that it took him several months to understand Rx. While being tutored by one of the creators of ReactiveX. It's "easy and simple" in the same sense as "Haskell is easy and simple" or "rocket science is simple and easy" or <any branch of human knowledge> is simple and easy once you know and understand it."
Rx is neither simple nor easy, for non-trivial projects. Its an incredibly leaky abstraction and you end up having to understand the internals to do non-trivial things. Understanding when something runs in what thread (and in RxJava knowing when to use subscribe/subscribeOn/observeOn was much harder than it claimed to be), how to correctly handle errors, retry failed operations, apply backpressure without dropping data — these things essentially force you (in my experience, at least, but I’m no Rx expert, just used it for a few months) to dig into the internals to understand how they work: ie not simple.
But because of its lack of simplicity, it was also incredibly hard to use, to make it do what you want. So it was neither simple nor was it easy.
(And yes, I buy into the differences between simple and easy)
Points 2 and 3 seem (to me, I hope I'm not misunderstanding you) to be "fixed" by clojurescript's re-frame. You get a single queue of events that you define, based on what is relevant for your application, and you structure your app to cascade based on that stream. Creating a loop is some part of the app adding an event to the queue. And you are forced to keep functional code that only stores state in the app store tree. So you can easily know which event sequence makes a bug appear.
Even though Rx/ReactiveX is not easy and simple, it is a big improvement over traditional event-driven programming. The ideas behind Rx are also more general than, say, the actor model. Unfortunately, it is also easy to make a mess with Rx, and when that happens, the debugger is of little help. I often end up writing stuff to the console.
I have not found hot vs cold observables to be a problem in Rx (for C# or Java; I have never used RxJS). In fact, I often use "semi-hot" observables, such as what you get from a BehaviorSubject, which initially gives every new subscriber the latest/current value.
To me, the fact that "the concept of time may be screwed" when using Rx, is a good thing because of the flexibility it gives. In particular, it can be very useful for testing; but I do struggle with Rx scheduling at times. At some point there will hopefully be more information available on how to use Rx effectively in complex, real-life scenarios. Maybe someone should write a book on Rx design patterns?
Point 2 (and the difficulty of creating backpressure[1]) is what made my team rip RxJava out of a project and replace it with something resembling clojure’s channels (well, really queues that can be “closed” which would unblock any thread waiting on it). Awkward error handling too (errors typically killed a stream, re-establishing it was not always easy). We needed complex retry logic, which meant we needed to detect errors and funnel the error values back to the source to be retried later, after some timeout perhaps.
Maybe we were using RxJava wrong or weren’t knowleadgeable enough, but that’s part of my point: all of this was really hard for us in Rx and really easy once we could connect our logic through queues. Our code after ripping Rx out of the codebase looked quite similar: you could do queue.map(..).filter(...).moveTo(otherQueue) an so on. Its not identical to Rx, but its close enough that it only took 2 days to replace Rx with this, even though the project was quite large. You shouldn’t have to be an expert in Rx to use Rx. After about 4 months of using Rx every day, I was actually more confused than I was on day 1.
Another issue we had with RxJava was that stacktraces almost never pointed to where the error actually happened in code, because the context of where the error happened in code is the context of when the streams are set up, NOT the context of where the streams are executed (and RxJava’s constructs are incredibly deeply nested too, making the stacktraces huge).
We looked at a number of alternative libraries, but they were all too complex for our needs so we ended up writing a simple wrapper around queues that allowed us to “close” the queue (unblocking anything waiting on putting or getting) as well as to “complete” the queue, which would close the queueu for new puts but not for gets until its been drained. This took about a day for the initial implementation and two days to refactor our codebase to use this.
> - My favourite thing: everyone tells you how easy and simple Rx is
I joined the project and thought “wow! Rx is so simple! I love t!” and three months later, I hated Rx because of how complex it actually turned out to be. Rx is NOT simple for real world needs (back pressure that doesn’t drop data, error handling, retry logic)...
[1] RxJava has built-in backpressure support, but it was quite limited unless you jumped through hoops to implement your own data sources (sorry, I don’t remember the lingo). You could easily add throttling that would drop values, for example, but we found it really hard to have a->b and have a block until b can accept more. We first wrote a queue-based thing that would work with Rx since the queue would block if full, but it didn’t play well with Rx in the face of errors (if the stream died, then items on the queue got lost, iirc) and it was really hard to fix. Ripping out Rx and replacing it with our own little thing fixed all of these issues for us.
It is really interesting (and unfortunate) that you found it so hard to solve these issues within the framework of Rx(Java). Do you think they could be solved with minor adjustments to the framework, or is there a fundamental problem?
It’s possible that it could be solved, sure, but RxJava is complex and has so many layers of objects that it’s very difficult to trace what’s going on. As was mentioned above, the difference between hot and cold subscriptions and how hard it is (at least it was for me) to know what runs on what thread, all of these issues made it incredibly difficult to work with and I don’t see these things going away as they’re caused by fundamental design choices.
It would be easier, in my opinion, to design something that is like Rx and has a very similar api or feature set, but is architected to avoid these issues. We ended up building a system from queues which supported many of the operations Rx did (map, filter, join, merge, etc). I will say that our implementation wasn’t super efficient in terms of thread usage (it creates more threads than really necessary), but that could have been solved, it just wasn’t necessary for our needs.
To rephrase a quip, I have found often that RxJS and similar tooling is the worst way to get correct behavior... except for all the others.
More than once I have set down a path to avoid RxJS, because of the complexity of explaining it and bringing a new developer up to speed... and ended up writing more code and having more difficulty producing correct results.
My context is in web applications, SPAs. Typically with Angular, but I've used many tools in this space, including React, Redux, Cycle (which has its own Rx-like library), etc. The tricky part in all this is pervasive asynchronicity.
Here's an example. Looking back at the AngularJS era, most of the applications I have seen (including review of many of our big customers applications) have incorrect behavior in the case of servers with jittery response time. It is quite easy to make an event handler load some data, and miss the fact that the data arrives in a different order than it was requested. Very easy to avoid with RxJS. If anyone's interested in this bit, here's a video explaining the merit of RxJS even with AngularJS 1.x, before the adoption of RxJS in Angular 2+.
I "learned" RxPy in a couple hours yesterday (a 3rd party lib I'm using has an interface that requires me to use them), and I figure I'll share a bit about my experience:
3) it's actually quite simple once you figure out it's just code level pub/sub with a new naming convention.
4) there seem to be some opinionated perspectives around using it in the community, though largely from a JS perspective (look into "should I use subjects").
Despite those flaws, I've found it pretty accessible, and I'll actually switch out some home-grown code for it.
[edit] I noticed some other comments complaining about execution handling (threads, etc.) so I figured I'd update this and say, maybe I got lucky as it's all pretty straightforward in Python (as long as you understand your app's execution environment (threads, asyncio, etc.) -> https://github.com/ReactiveX/RxPY#schedulers
Whenever you abstract a data structure, you loose some meaning, some peculiarities that helps you understand what you’re doing.
Rx should be used by framework or low level libraries developers, to factor code. But it should be almost invisible to the api user.