CSP vs. FRP
Some time ago I stumbled across David Nolen's post on Communicating Sequential Processes. Since then, Nolen's actually written a number of articles on core.async and CSP. With each new article on CSP, I'm increasingly realizing that I'm missing something about the idea. On the surface it seems very similar to Functional Reactive Programming, a paradigm that I'm pretty familiar with. As a matter of fact, I'm certain that I could reimplement many of his examples using FRP approaches. Yet Nolen consistently emphasizes that the two are not the same and that CSP holds some clear advantages over FRP.
So, I'm going to put David Nolen to the test. I'm going to recreate his examples using bacon.js amped with the yolk wrapper for sweet ClojureScript goodness. I plan to go through as many of his posts as possible until I reach the point where either FRP fails completely, or the FRP solutions become so weak that it's clear that CSP has the upper hand.
To be clear: my goal here is not to prove Nolen wrong. I actually hope the opposite happens. My goal is to realize something about CSP that gives me deeper insight into the Art of Programming.
And so, without further ado, I give you the first set of examples.
Before we get started, here's the
ns for context:
(ns blog.frp (:require [jayq.core :refer [$] :as j] [yolk.bacon :as b] [yolk.jquery :as bjb]))
Nolen's first example has three independent processes running at different speeds. These processes are coordinated and displayed by a fourth process.
Much like it's CSP counterpart, this solution has three streams that are merged into a resulting fourth stream for rendering.
(-> (b/merge-all (b/interval 250 1) (b/interval 1000 2) (b/interval 1500 3)) (b/map #(str "<div class=\"proc-" % "\">Process " % "</div>")) (b/sliding-window 10) (b/map (comp into-array reverse)) (b/on-value #(j/html ($ :div#example-multi-process) %)))
The thing I noticed here was that the FRP solution seems much more elegant that the CSP solution. Everything's streamlined into a single statement, and the logic is more straightforward in my opinion.
The next example displays the mouse's coordinates in a div:
(defn offset-stream [$elem] (-> (bjb/mousemoveE $elem) (b/map (juxt offset-x offset-y)))) (let [$elem ($ :div#example-mouse-element)] (-> (offset-stream $elem) (b/map (fn [[x y]] (str x ", " y))) (b/on-value #(j/html $elem %))))
The code for
offset-y is at the bottom of this post.
Both solutions are fairly simple in this example: take a stream/channel and update the div as events come through.
The next one maps the previous example to page coordinates rather than div coordinates. It's a bit more interesting for the core.async example since Nolen introduces a home-built
map function. It's a little less interesting for the bacon example since
b/map is built-in, and it's been used in both of the previous examples.
(defn page-position [$elem [x y]] (let [offset (j/offset $elem)] [(+ x (int (:left offset))) (+ y (int (:top offset)))])) (let [$elem ($ :div#example-mouse-page)] (-> (offset-stream $elem) (b/map (partial page-position $elem)) (b/map (fn [[x y]] (str x ", " y))) (b/on-value #(j/html $elem %))))
The next example shows off async's ability to coordinate two channels in a single event loop. Since there is no analog for event loops in bacon, I decided to combine two streams using
(let [$elem ($ :div#example-mouse-keyboard) mouse-stream (-> (offset-stream $elem) (b/map (partial page-position $elem)) (b/map (fn [[x y]] (str x ", " y))) (b/to-property "") keyboard-stream (-> (bjb/keyupE ($ js/window)) (b/map #(.-keyCode %)) (b/to-property ""))] (-> (b/combine-as-array mouse-stream keyboard-stream) (b/on-value (fn [[pos-string keycode]] (j/html ($ :span#emk-mouse $elem) pos-string) (j/html ($ :span#emk-keyboard $elem) keycode)))))
One thing I noticed here is that the core.async example was forced to resort to a switch to coordinate between the two loops. Although that's certainly possible in bacon, you would have to go out of your way to do it, which is a plus in my opinion.
This was also the first time that I hit a bit of a snag with the FRP example. In order to properly coordinate the two streams via
b/combine-as-array I had to change from using event streams to using properties. This is because one event could fire before we ever receive an event for the other, so we need to "intialize" both streams with the empty string. I'm not certain whether to knock bacon for this since it gives you all the tools you need to resolve the issue, but it's a little bit of a hassle having to learn about the different kinds of observables.
The final example demonstrates parallel searching with timeouts:
(defn fake-search [kind] (fn [query] (b/later (rand-int 100) [kind query]))) (def web-1 (fake-search :web-1)) (def web-2 (fake-search :web-2)) (def image-1 (fake-search :image-1)) (def image-2 (fake-search :image-2)) (def video-1 (fake-search :video-1)) (def video-2 (fake-search :video-2)) (defn fastest [query & replicas] (let [bus (b/bus) timeout (b/later 80 "null")] (doseq [replica replicas] (b/plug bus (replica query))) (-> (b/merge timeout bus) (b/take 1)))) (defn google [query] (b/combine-as-array (fastest query web-1 web-2) (fastest query image-1 image-2) (fastest query video-1 video-2))) (-> ($ :button#search) bjb/clickE (b/do-action #(j/prevent %)) (b/flat-map-latest #(google "clojure")) (b/on-value #(j/text ($ :div#example-search-output) %)))
My solution ended up very similar to Nolen's. Part of reason for that is I wanted to be able to directly compare the syntaxes. The other reason is that I'm not certain there are very many other ways to solve this problem when you're working with stream/channel-like constructs.
I think it's worth noting that, unlike the core.async example, this solution will return the three results in the same order each time due to the behavior of
So far I prefer the FRP solutions to the CSP's. The bacon solutions tend to be at least as concise as the core.asyc solutions, usually slightly more concise, and, in my opinion, they tend to be more straightforward. However, to be fair, I'm more familiar with bacon, and I knew going in that all of these solutions were imminently do-able with bacon. Also, Nolen notes in his posts that FRP enthusiasts would recognize all of these examples in this post, but said that "the good stuff" was coming. I look forward to it!
Here's the code that I had to use to get the mouse offsets. This wouldn't have been necessary if, like Nolen, I'd used the Google Closure library.
(defn offset-x [e] (or (.-offsetX e) (- (.-pageX e) (int (:left (j/offset ($ (.-target e)))))))) (defn offset-y [e] (or (.-offsetY e) (- (.-pageY e) (int (:top (j/offset ($ (.-target e))))))))