raquo / Airstream

State propagation and event streams with mandatory ownership and no glitches
MIT License
247 stars 28 forks source link

Regarding Functional Reactive Programming #80

Closed cubuspl42 closed 3 years ago

cubuspl42 commented 3 years ago

Hello,

thank you for creating this library! It looks very interesting and promising.

I've noticed you used the term "FRP" in the description, and obviously the library seems to be an FRP implementation (or at very least, strongly inspired by FRP).

What's your FRP background? What are your opinions on FRP? Are you basing your work on Conal Elliot's FRP semantics? What other FRP solutions is Airstream inspired by?

I'm asking out of interest; I'm very interested in practical FRP implementations, especially all efforts that incorporate mixing reactive operations with I/O.

raquo commented 3 years ago

Hi, I don't really have much of an FRP background. Airstream is mostly a result of practical pain from trying other solutions.

Originally I got into all this after trying Cycle.js and thus Andre Staltz's work. In fact, the first version of Laminar used his xstream.js, before I made Airstream. And Airstream's internals take some inspirations from Xstream, including shared execution and just the basic model of observables and listeners, as I've read the xstream.js source code to understand how such libraries are implemented.

The way Laminar binds FRP to the DOM was originally inspired by Outwatch, but then I moved away from virtual dom because it doesn't interact very well with virtual DOM (more on this here). I knew it was possible because I've used knockout.js before which has somewhat similar ideas, but also because of all my Laminar work to date I already knew what needs to happen under the hood.

Scala.rx was another inspiration especially for signal semantics and error handling.

I didn't just borrow the good ideas from all these libraries but also did things differently than those libraries because they didn't work for me. For example, dealing with MemoryStream in xstream.js and seeing the filter caveat in Scala.rx made me separate the concepts of signals and streams.

I don't quite remember what exactly inspired the ownership feature. The need for it was obvious as I hated to do manual cleanups of subscriptions, but I don't remember how I came to it. Certainly didn't copy it from any other streaming / frp library, I'm not aware of any that have a similar mechanic. Although Scala.rx has something like that, even if it's hidden from view by macros and stuff. So maybe that.

Same for using topological rank to prevent (or, well, localize, if we're being completely honest) FRP glitches – don't quite remember how I arrived at this solution. I've done some research, skimmed some papers and stackoverflows. Looked at a few streaming libraries. Pretty sure Conal's work occasionally came up in those searches, but I don't remember what, if anything, I got directly out of it.

The split operator I came up with after trying to find a more ergonomic solution for rendering lists of children in Laminar. Essentially, I tried to apply the concept of keys from React.js to a library without virtual DOM.

Airstream also originally had a State type, a strict alternative to Signal, which was inspired by Scala.rx I guess, but that didn't work out due to memory management footguns, and was dropped.

cubuspl42 commented 3 years ago

Thank you very much for your answer.

made me separate the concepts of signals and streams.

In FRP they have always been separated, since 1997 ;) Surprisingly, till this day people feel the need to merge them, which I absolutely don't understand.

I don't quite remember what exactly inspired the ownership feature.

Ownership is a very interesting problem in FRP implementations (or even semantics!), I totally agree. To be honest, I haven't yet found time to fully analyze your solution, but I surely will. I personally have attempted to make progress in this field, but the best solutions are difficult to implement in most languages, sadly.

Same for using topological rank to prevent (or, well, localize, if we're being completely honest) FRP glitches

What do you mean by "localize"?

Yes, there are a few ways to eliminate glitches. I assume you use the priority queue based approach, like in Deprecating the Observer Pattern? I personally had really bad experience with the Sodium JavaScript implementation of that algorithm, I had (by orders of magnitude) better performance results using nearly brute-force push-pull approach, which I found intriguing. It's possible that there was something very inefficient with Sodium implementation, but I don't know what. I tried to replace the priority queue with other implementations and it didn't really help.

raquo commented 3 years ago

What do you mean by "localize"?

Docs explain that in more detail, but: Airstream has no FRP glitches within a Transaction. However, when you make EventBus emit an event, that always happens in a new transaction. Same for Var, same for the flatten / flatMap operators. The reason is the same – this functionality allows you to create loops of observables, and so my topological rank implementation doesn't work anymore as it assumes that observables form an acyclic directed graph (they do, but only within a transaction. With event bus or other methods you can make an observable depend on itself).

And so, what I mean by "localize" is that Airstream can have FRP glitches, but only transaction boundaries can cause them. Well, perhaps "localize" is not the best word for that. Just wanted to point out that glitches are not completely eliminated in Airstream.

In practice though this approach has completely resolved the issue of glitches for me, but that's because I don't use EventBus-es and such without a good reason. And those good reasons tend to preclude the possibility of glitches even despite the lack of built-in protection, e.g. using an EventBus to collect user events from independent children components means that those events are independent from each other, and so will not happen in the same transaction, and so EventBus will not screw up the timing or cause additional glitches by creating a new transaction for each of those events.

I assume you use the priority queue based approach, like in Deprecating the Observer Pattern?

Yes, I do use a priority queue based on topological rank, and I did read that paper early on (must have found a reference to it in Scala.rx docs), so that must be where I picked it up! I don't remember what exactly the paper says now though.

Re: performance, I don't think there are enough events flying around in the browser environment to cause a significant slowdown from the priority queue. In Airstream topo rank of an observable is static. The priority queue only lasts for the duration and scope of the Transaction. How many observables do you need to combineWith, all of them emitting an event in the same transaction, for ranking them by a static integer on such an event to become a performance problem? I haven't run the numbers, but I don't expect it to be a significant problem for typical Airstream use cases.

PS moving this to discussions, just like the others. Let's continue there.