Core.async channels vs Rx/Observables


#1

Hi,

Trying to understand the motivation of core.async better, I re-watched Rich’s original talk introducing the library, where he lays out the purpose behind it.

Early in the talk (04m55s), he discusses problems with other ways of writing programs that need to use a lot of asynchrony because they need to be highly reactive to external events (like network traffic or user inputs).

His slide has these bullet points:

  • Listenable Futures/promises
  • a web of direct-connect relationships
  • difficult to reason about or control flow
  • fragmented logic - callback hell
  • handler list visibility, monitoring, control difficult
  • Observabls/Rx etc only mitigate certain cases.

I would LOVE to understand this slide completely. I think I only understand it partially at the moment. :blush:

One that interests me in particular is the last item, “Observables/Rx etc only mitigate certain cases”. Could you say a word about this? In what way are they more limited than core.async-style channels? Rich says the following:

And there have been, you know, various approaches to try to mitigate some of this with observables and Rx and things like that, but they only handle a very narrow set of cases, mostly, you know, filtering or making a stream like kind of approach to composable transformations on a single event chain. But if you really are trying to make a state machine that has multiple sources and sinks of events, you can’t just get it out of something like, you know, filter and map composition primitives.

So Rich is saying that “observables and Rx and things like that” don’t provide primitives for making a state machine, because they don’t easily handle multiple sources and sinks of events.

What puzzles me about this, is the the implementation of Rx-like ideas that I am most familiar with is ReactiveCocoa, a reasonably popular translation of Rx into Cocoa for UI development. I have never seen ReactiveCocoa discussed in the same breath as golang channels or core.async. I am dying to understand if that’s because they are fundamentally different in some way (which is what Rich seems to be implying) or if it is just accident that no one has pointed out they are the same thing.

If you look at this page summarizing the ReactiveCocoa operators, it sure seems like there are facilities for handling multiple streams.

Was Rich discussing narrower implementations than ReactivceCocoa? Or am I missing something basic here?


#2

Hey Alexis,

Good question.

Now, I can’t speak to what Rich Hickey was actually referring to, but I can point to something in my experience that I don’t see in other FRP/Rx libraries.

In ReactiveCocoa, you have operators that can act on multiple streams, but I don’t see anything in there that lets you actively select which stream you’re reading from while ignoring others. For instance, in core.async:

(go
  (let [command (<! control-channel)]
    (case (:type command)
      :update-user (<! (update-user))
      :drain (loop []
               (when-let [val (<! (:values command))]
                 (save val)
                 (recur)))
      ...)))

In the above example, the go block waits on the control channel, and when it receives an “update-user” command, it runs update-user and waits for it to put on another channel. But if it gets a drain command, it reads all values from the channel the command specifies. Notice that new commands may be coming in, but the go block actively chooses to drain everything first, before receiving a new command. This control flow is the state machine he’s talking about. The states are places where the code is waiting for a channel, and the code in between is the transitions. I believe the go block compiles to something very much like that.

So, succinctly, instead of being “reactive”, meaning callbacks that can be called at times beyond your control, core.async gives your go blocks back that control.

Eric


#3

Thanks so much.

This is a fascinating and I think very deep vein of inquiry.

Over in iOS land, some people get very excited about being more “reactive” but I think they often haven’t really thought very deeply about the matter, since it seems to mean quite different things to different people. And a lot of the alternative GUI architectures they advocate could be more easily described as the more modest advice of just “be a bit more orderly about how you map model values to UI state”. This does not make a great motto.

More interesting, I think, are efforts to be more structured and explicit about managing the unavoidable welter of state you see in complex UI. I’ve always been curious about the possibility of being much more explicit about state through the use of state machines.

It had not occurred to me to think of the stream-like value processing that CSP enables is notable explicitly because it was a way to build state machines. I’ll need to mull this over!


#4

Great discussion. Another comparison I’d like to see is between core.async and Elixir/Erlang.