Hacker News new | comments | show | ask | jobs | submit login
An introduction to Reactive Programming (gist.github.com)
357 points by jxub 8 months ago | hide | past | web | favorite | 72 comments



> If you prefer to watch video tutorials with live-coding, then check out this series I recorded with the same contents as in this article

Thanks for actually writing. Written information can be parsed non-linearly, which is super important for learning and reference. For whatever reason, a lot of people don't seem to understand that, and just put out videos, which are like lectures: helpful, but frustrating.


Both can be useful - especially if video is with live coding. Texts often omit the small details that are perhaps obvious to the author, but you are not aware of them. IMHO every installation instruction out there that takes more than copy&pasting 2 lines of text should be in a form of a video, because there's always that one step that is not in the text...


It also helps to actively do the thing the way you expect the reader/viewer to do it, so that you don't skip any steps, and you understand common pitfalls.

The problem with doing that, however, is that you are doing something a specific way, on a specific platform, using specific tools. That can make it frustrating or even impossible for viewers/readers to break out of your specific workflow/tools/platform, which is usually what they want to do.


"Well, obviously, you needed to press CTRL+Q+F4 between steps 2 and 3, it's so obvious we don't even need to put it in the instructions."


Lectures, for me, are a great touchpoint, and, in the context of academia, a great way to set (and maintain) a rhythm. But they (almost always) require me to actively take notes (something I can't always do) to really take anything of value away from them.

(All of that is to say, I agree with you.)


> For whatever reason

Just a wild guess, but that reason could be that ad monetization works way better on videos than static pages and are more likely to garner subscribers to build a following.


That's an excuse, not a reason.

Reasons are dependencies. Excuses are orthogonal.


My shop specializes in multiplatform FRP applications via the reflex haskell library [0].

After many years of wrangling FRP my biggest learned lesson is certainly this: not everything is a stream! You need to be able to mix streams (also called events), which embody push semantics and behaviors (things you can sample), which embody pull semantics. This is important for performance and for the structure of large applications.

Coming up with a set of primitives to efficiently combine events and behaviors in expressive ways is really difficult but worth it.

[0] https://github.com/reflex-frp/reflex-platform


> After many years of wrangling FRP my biggest learned lesson is certainly this: not everything is a stream! You need to be able to mix streams (also called events), which embody push semantics and behaviors (things you can sample), which embody pull semantics.

I have a creeping suspicion that after a few more years, "push" is just going to be viewed as codata, and "pull" will just be "data". I mean, that's kind of what they are, our languages just don't have codata as first-class citizens, and so we shoehorn it into stream abstractions.


I believe that the distinguishing property is not what the event/data/codata is, rather how you are accessing it and when you process it.

In a "push" model you provide a callback that will receive events and it has no way of influencing their frequency or timing. There is no mandatory buffering here.

In the "pull" model, you have a handle, that you can call to receive the next event, which is essentially an iteration over async events. It is up to you to decide the frequency or the timing, and it is up to the backing library to buffer events if needed to be.

I'm linking to the Dart API pages here, but the same concept applies to every language/library out there. Stream.listen [0] is straightforward and similar to many other libs out there. The interesting part is that Dart has a StreamIterator[1], which allows you to "pull" events form the stream at your own pace.

[0] https://api.dartlang.org/stable/1.24.3/dart-async/Stream-cla... [1] https://api.dartlang.org/stable/1.24.3/dart-async/StreamIter...

It is also easy to use Stream Iterator to loop over the values unconditionally:

    Stream<Event> stream = whatever();
    var iter = new StreamIterator<Event>(stream);
    while (await iter.moveNext()) {
      // process the iter.current value at whatever pace you'd like
    }


> I believe that the distinguishing property is not what the event/data/codata is, rather how you are accessing it and when you process it.

But this is exactly one of the important distinctions between data and codata. If your language supported first-class codata, you wouldn't need to encode push evaluation via a stream data type. Codata already has the required push-like semantics, and your language's evaluation model would handle the necessary plumbing for you.


Could you unpack this a bit for the relative layperson? I once heard a guy at a Haskell meetup say that “codata is the categorical dual of data” but I didn’t know what that means then and I still don’t know.


Data is finite data structures, while Codata is infinite data structures (like streams.) They are duals because you can generate and process data using induction, while you can use coinduction to generate and process codata. One is "built up" naturally, and the other is "torn down" naturally.


To elaborate a little on the other poster, the data types you're used to are finite values built incrementally, ie. build this, then build this, then build that.

Codata is the inverse, in that you can see it as a finite set of observations of some already built infinite data structure, ie. unpack this value at the head, then unpack that, etc. For instance, the stream of values observed from a process that runs forever.

We usually encode codata using inductive data types, but this is awkward for all of the reasons that come up in FRP and other reactive systems.


The intuition is close but it's not quite the same duality as between data and codata. It's probably true that Behavior and Event are dual but they should be viewed more like modalities than as data structures. In fact, a first cut at semantics for FRP is temporal logic which has modalities like "sometimes" and "always".


>codata

I'm not familiar with the term. Could you please elaborate?


See other replies to my comment for more details.


Flapjax had behaviors, as you describe, as a key facility in a JavaScript FRP implementation, dating back to 2008:

https://github.com/brownplt/flapjax/blob/f92e99ea25a4533c4ab...

I "discovered" FRP in 2011, when I stumbled upon Flapjax — spent a month pouring over the source code, learned a lot, my brain has never been the same since!


Sounds about right, FRP was originally formulated around events and behaviors all the way back in 1997! Great credit to Conal Elliott and the late Paul Hudak for that.


One of the teams I was on solved the pull behavior by having an "immediately publish last known value on subscription" parameter we could pass in when subscribing.

This solved most of our needs around pulling data.

We weren't doing a full FRP system, no composable streams, just building a system in which the only way to communicate was through subscriptions between components.

This worked really well in practice, for example to configure the rate at which the accelerator ran, a component would subscribe to the proper hz publisher, with different publishers existing for supported frequencies. A fancy bit of code handled doing the math needed to deliver all the different subscribers data at their requested rate.


So its like an rxjs BehaviorSubject?


Not quite sure, haven't looked into rxjs much.

We were running on an embedded micro with 256KB of RAM, so just making everything async and callback based was already pushing the boundaries of the industry. Vendors we worked with got extremely confused when we said they needed to hand us async code, that they had a few milliseconds to run in before they relinquished control, and that busy loops were verboten.

I really miss working in a code base where every component can be trivially subscribed to.

On the flip side, I've seen subscriptions taken too far, as people make every single value on a form a separate subscription. Seriously, it is A-OK to give the First and Last names together when the user hits submit.


Since no one has mentioned it yet, and it respectably isn't mentioned in the article, the author of this is the creator of the cycle.js (cycle.js.org).

It is a really cool way of looking at DOM state as a flow of: <interface> -> user interaction -> application reaction -> <interface>

The general idea being that the state of an application can be represented by a timeline of events throughout the lifetime of an application. Since it is entirely time and event driven/stateless, it gives way to some pretty cool "time travel" concepts.

The philosophy is this:

If you know how something begins, and you know the exact times at which anything happens to it, then you can assume its state at any given period in time.


IMHO, you run into the same problem as journaling. User interaction is basically a stream of deltas. If everything is perfect, it’s awesome. But really, you want checkpoints from time to time.

It’s a cool idea. It’s cooler if your organization has the chops to rarely introduce defects. Replaying on refresh works ok ish. The really cool part is making every interaction with the system uniform. State comes in, small tweak, state goes out.


While this may be technically sound, I think this is not the most fundamental way of explaining FRP. FRP is just a toy really. It's a useful toy that help to grasp and use an advanced programming concept. Let me explain.

That concept originates from a common programming mistake. A common mistake is to use compulsive caching. It's the tendency to store function results in a variable just because this result is gonna be re-used. The right way instead is to evaluate (or "re-evaluate") the function every time you need it unless you have a good reason not to (the only good reason is that the function is a bottleneck that affects the experience of the user). This is a better way of doing things because as everybody knows, invalidating caches is a hard thing to do (and variables, guess what, are just caches).

The added benefit (which really is not a benefit, but just good programming) is that things gonna now seem reactive.


This "assignment is caching is bad" analogy does not hold any water.

You get a result from a call to `foo(bar)`. You have to _do something_ with that result. One common thing you'd need to do is to call another function: _baz(foo(bar))_. But then you're just hiding the assignment inside a baz function call (the result of _foo(bar)_ will be available as a variable/constant inside of _baz_).

That doesn't so bad if you only need to do one thing with with the result of `foo(bar)`. Now what if you have to do something with that result twice. In this case all you've done is reduce the scope of a constant to be smaller than it needs to be, requiring you to make a duplicate `foo(bar)` call.

Now you're repeating yourself and you don't know anymore if you're conceptually dealing with the same result or if foo(bar) might produce a different result on second call. You removed philosophical/semantic meaning from your code and did not gain anything from it.

If you find yourself wanting/needing to "expire" variables that you define before you run out of scope, then the answer is a combination of:

1) use constants of immutable data structures, not variables or mutable structures 2) better name such constants so that you know what they represent 3) better manage scope and function size

In practice, the above is much easier to achieve than what you're proposing, and makes the code easier to read and maintain.

> The right way instead is to evaluate (or "re-evaluate") the function every time you need it unless you have a good reason not to (the only good reason is that the function is a bottleneck that affects the experience of the user)

That does not scale to using the coding paradigm that you're proposing application-wide. Your whole app will be one big bottleneck because it's doing a multiple of the computations that it needs to be doing, with no easy way out. I guess you can get away with that by throwing more money at your backend infra, but you can't do that on mobile or for web frontends or desktop apps.


Good point. It's not really relevant for local variables I guess, because those caches are invalidated quite elegantly (you'll still find cases Im sure where the programmer took that too far). I dont have all the cases in mind but, for example, storing results in object members is common and more often than not, detrimental. Also, there is the events. Events are a caching mechanism as well. They are an optimisation and they are evil. Now don't get me wrong, if you have a bottleneck, events can be a great optimisation.

The whole point being, FRP, while having some cool features, is mostly fixing bad programming practices.


> You get a result from a call to `foo(bar)`. You have to _do something_ with that result. One common thing you'd need to do is to call another function: _baz(foo(bar))_. But then you're just hiding the assignment inside a baz function call (the result of _foo(bar)_ will be available as a variable/constant inside of _baz_).

Unless the OP is arguing for call-by-name evaluation, which when you take his words literally, sounds like exactly what he's arguing for.


Call-by-name is just syntactic sugar for anonymous functions. It has all the same problems.

But more importantly, the whole premise is wrong. "Cache invalidation" for data that has an average lifespan of 30 lines of code in a single function is not a hard problem. Real cache invalidation is hard for reasons that don't translate to the case of ephemeral constants with properly limited scope.


> Call-by-name is just syntactic sugar for anonymous functions.

No, it's not. It defines different evaluation semantics.


Which are exactly equivalent to a function call, just with a different type, and a syntax without parens.

"this will be re-evaluated every time you access it"


I see what you meant now. Though it seems you are mixing notions of anonymous function and function as an argument of higher order functions (it doesn't have to be anonymous).


Your world does not address the difficulties of dealing with shared state and coordination, which is what FRP addresses in a principled way. FRP means you don't have data races on checking global variables and events littered all throughout the program, firing and mutating in an order that must be considered effectively random.


Function composition do most of that. Now, I said it was a fundamental explanation, not necessarily the whole story.


That is quite hand-wavy, and I don't think adequately addresses the issues.


This is an interesting article, but I don't think it gives enough credit to Functional Programming. The article presents functional programming applied to push streams, which is part of FRP but only one way to look at Reactive Programming.

Thinking in streams is a great exercise but is not a panacea. It's really easy to get started with streams in a toy example, but in real world applications backpressure and buffering are a concern. The consideration of backpressure also opens the push/pull stream discussion. If you are interested in applying more from this article, the API is very similar in Godot2 (Node) [0], which is based on Riemann (Ruby) [1]. (Node streams have configurable backpressure/buffering [2].)

Complex interconnected events can cause a runaway snowball effect if not carefully constructed or bound by limits:

* Elm's "time-travel" debugger is so valuable because it allows introspection through an event chain that may not be linear.

* Node takes a common approach, which is to push data/events through the stream but handle errors by bubbling up the stream processing chain. This Node approach maintains a hierarchy that is relatively easy to reason about.

* React is popular because it effectively is FRP applied to the data model: the JSX view is the function, and the change in data model is the event. Since data model changes are limited, atomic, and one-way this makes React easier to reason about than other frameworks (Angular, I'm looking at you). This concept pre-dates React, I've used it since years ago with SpineJS and Knockout.

Last but not least, when using streams there is an important tradeoff between speed/latency and delivery/processing guarantees, which is enough for its own discussion...

[0]: https://github.com/nextorigin/godot2

[1]: http://riemann.io/concepts.html

[2]: https://nodejs.org/en/docs/guides/backpressuring-in-streams/


Most of those aren't FRP, at least in the Elliott/Hudak sense. Or if you mean anything that pushes streams around is FRP, then of course that existed long before FRP was even a word and happens in non-functional paradigms as well.


Voice of reason. Thank you!

Synchronous dataflow, Lucid, Esterel, Supercollider :-)

FP does not solve a problem for reactive programming, reactive programming solves a problem for FP.


I agree. I intended to make it clear with my first sentence that Streams as described in the article is just a subset of Reactive Programming.

My point is that Functional, Reactive, and Streams programming have common concepts that are important programming tools, when used appropriately. However, Reactive Programming is neither exclusively Functional nor Streams-based, although the article implies it.


when talking about 'backpressure' it is essential to distinguish whether we are talking about pull streams or push streams, and if the backpressure mechanism is in-band or out-of-band.

usually when we talk about a 'reactive stream' we are only talking about push streams, because if you are truly reactionary the consumer doesn't control the flow like a pull-based iterator does.


Correction, Riemann is implemented in Clojure, not Ruby.


This is great, and if anyone is looking for additional examples in a library that is a bit different from the others check out Racket’s reactive programming library.

docs.racket-lang.org/frtime

This was how I was introduced to reactive programming 5 or so years ago.


previous discussion https://news.ycombinator.com/item?id=7964873

There was a lot of talk on what FRP (Functional Reactive Programming) really meant, and if Rx is really FRP.

My understanding these days is that Rx has distanced itself from the FRP term proper, while retaining the goals of programming with reactive streams.


Well, Conal has also distanced his work from the FRP term.


My point still stands.


Ah, streams. Or as I like to call them, "mysterious voids of side-effect riddled effervescent state".


I'm going to go out on a limb here with a prediction. Reactive Programming will not catch on in any big way. Maybe in 10 years I'll look foolish for having said this, but I'm willing to take the chance.


It's turned out to become a standard for mobile developers due to the concurrent and async nature of the platform. As a side note, I've been working through ~10 different code bases during the last 4 years - every single one has included an Rx library. Scala, Java, Swift, Kotlin, C++.

My bet it's here to stay.


>It's turned out to become a standard for mobile developers due to the concurrent and async nature of the platform.

A "standard for mobile developers"? Not that I can see.


On the contrary, it ties in neatly with platforms & programming models that can't just rely on being able to summon up an OS thread at will, but have to self-schedule over a small number of owned threads.

In just about every event-loop-driven, there exists an implementation of Rx (or something like that anyway)


Back when I was a kid, we called this rapid application development - but then again, we didn't create cults around dev methodologies...


> but then again, we didn't create cults around dev methodologies...

You sure about that? I certainly recall plenty of holy wars over object oriented programming.


And the 4GL bubble.


> Back when I was a kid, we called this rapid application development

What do you mean by "this"? RAD tools like Visual Basic, Delphi or Cincom Smalltalk were event-based - which is also true with most native GUI frameworks, BTW - but they did not provide any abstraction above the level of events. Reactive programming takes events and stuffs them down a set of pipelines, where previously you had this giant switch statement for dispatch and the logic for actually doing something was split among some of the branches of that switch. Now you can group your logic around a certain pipe, which allows you to see a complete description of complex processes at a glance, without the need to guess "which event is used as a next step of this process?"

So Reactive is actually a quite convenient interface to event-driven systems and it wasn't available in RAD tools of the nineties (if memory serves, might be wrong on this?)


I really liked Kris Kowal’s general theory of reactivity [1]. It’s a pretty in depth explanation of reactive programming in JavaScript that I found really illuminating. It goes through many different kinds of asynchronous data types and compares them to their synchronous counterparts along the way.

[1]: https://github.com/kriskowal/gtor/blob/master/README.md


This is awesome! Thank you for writing this Buzzword/Jargon free tutorial it's so much more helpful to have things explained in terms of concepts the reader already knows.

Good stuff!


Can traditional languages like C, C++ and even Object Pascal support reactive programming? Or this is only implementable in functional languages?


They absolutely can. Take a look at http://reactivex.io for implementations in lots of imperative/oo languages. Functional reactive programming can also be done in these languages but may lack some of the convenince of a functional oriented language


You can check here for a list of existing languages/frameworks which make use of Rx

http://reactivex.io/languages.html


What should we call original FRP now since Stream/Flow based programming has highjacked the term?


Pardon my blunt question, but who is the heck is this for? Is this a sideways method of pushing React.js? Or is this for assembly programmers as well, and I just don't get it?


It has nothing to do with React (in fact, React, despite its name, does not represent the paradigm of reactive programming).

This is for javascript programmers who enjoy functional programming and want an introduction into Rx.js library. It may also be of interest for programmers in other languages (because rx has been implemented in many of them).


I've thought this would be a good way to write video games, but most engines don't support the sort of functional programming that would make this pleasant to use, so I wind up creating events which solve similar kinds of problems. For games, the primary benefit would be to decouple various game components so you can create and remove them without having to worry about a complex web of dependencies and state. It keeps things declarative, which also helps the web of interdependedness problem of many, overlapping systems.


> "this new thing called Reactive Programming"

Let's take an age-old paradigm and present it as something new... why, I don't know, maybe so we can come across as an expert on something "new" and high-tech for fame and fortune...

...at least until someone who has been around the block comes along and figures out you're at best a bullshit artist and at worst completely ignorant and unqualified to be presenting...


I think that line is aimed towards people new to all this stuff, hearing "functional reactive programming" being thrown around. The author does seem to know the 'roots', if you can call it that:

"Reactive programming is programming with asynchronous data streams.

In a way, this isn't anything new. Event buses or your typical click events are really an asynchronous event stream, on which you can observe and do some side effects. [..]"


Ok where did you write reactive back in the day?


I don't think it was called reactive back in the day, but the ideas are old to some degree. If you drop the functional aspects, then "Reactive" programming goes back to Lotus 123 (for the those too young, think Excel).

Data base triggers is also "Reactive", and materialized views are also "reactive".

Are there some novel aspects to "FRP", sure.


And OS interrupts are also reactive?


Can you compose incoming OS interrupts?


In languages such as Esterel, Lustre, and Signal, in the late 80s


From what I've read so far it sounds like defining a set of transform functions which are executed whenever data arrives for their input parameters, creating output values.

Is it different?


Reactive is garbage: 1. Has no point. Literally no reason for ovbservables over promises/events. 2. Very difficult to debug. 3. All the "good" things, like pure functions, are not exclusive to it.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: