A taste of Reactive Programming
This example is built in ClojureScript and uses HTML 5 Canvas for rendering and RxJS (see https://github.com/Reactive-Extensions/RxJS)—a framework for Reactive Programming in JavaScript.
Before we start, keep in mind that we will not go into the details of these frameworks yet—that will happen later in this book. This means I'll be asking you to take quite a few things at face value, so don't worry if you don't immediately grasp how things work. The purpose of this example is to simply get us started in the world of Reactive Programming.
For this project, we will be using Chestnut (see https://github.com/plexus/chestnut)—a leiningen template for ClojureScript that gives us a sample working application we can use as a skeleton.
To create our new project, head over to the command line and invoke leiningen as follows:
lein new chestnut sin-wave cd sin-wave
Next, we need to modify a couple of things in the generated project. Open up sin-wave/resources/index.html
and update it to look like the following:
<!DOCTYPE html> <html> <head> <link href="css/style.css" rel="stylesheet" type="text/css"> </head> <body> <div id="app"></div> <script src="/js/rx.all.js" type="text/javascript"></script> <script src="/js/app.js" type="text/javascript"></script> <canvas id="myCanvas" width="650" height="200" style="border:1px solid #d3d3d3;"> </body> </html>
This simply ensures that we import both our application code and RxJS. We haven't downloaded RxJS yet so let's do this now. Browse to https://github.com/Reactive-Extensions/RxJS/blob/master/dist/rx.all.js and save this file to sin-wave/resources/public
. The previous snippets also add an HTML 5 Canvas element onto which we will be drawing.
Now, open /src/cljs/sin_wave/core.cljs
. This is where our application code will live. You can ignore what is currently there. Make sure you have a clean slate like the following one:
(ns sin-wave.core) (defn main [])
Finally, go back to the command line—under the sin-wave
folder—and start up the following application:
lein run -m sin-wave.server 2015-01-02 19:52:34.116:INFO:oejs.Server:jetty-7.6.13.v20130916 2015-01-02 19:52:34.158:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:10555 Starting figwheel. Starting web server on port 10555 . Compiling ClojureScript. Figwheel: Starting server at http://localhost:3449 Figwheel: Serving files from '(dev-resources|resources)/public'
Once the previous command finishes, the application will be available at http://localhost:10555
, where you will find a blank, rectangular canvas. We are now ready to begin.
The main reason we are using the Chestnut template for this example is that it performs hot-reloading of our application code via websockets. This means we can have the browser and the editor side by side, and as we update our code, we will see the results immediately in the browser without having to reload the page.
To validate that this is working, open your web browser's console so that you can see the output of the scripts in the page. Then add this to /src/cljs/sin_wave/core.cljs
as follows:
(.log js/console "hello clojurescript")
You should have seen the hello clojurescript
message printed to your browser's console. Make sure you have a working environment up to this point as we will be relying on this workflow to interactively build our application.
It is also a good idea to make sure we clear the canvas every time Chestnut reloads our file. This is simple enough to do by adding the following snippet to our core namespace:
(def canvas (.getElementById js/document "myCanvas")) (def ctx (.getContext canvas "2d")) ;; Clear canvas before doing anything else (.clearRect ctx 0 0 (.-width canvas) (.-height canvas))
Creating time
Now that we have a working environment, we can progress with our animation. It is probably a good idea to specify how often we would like to have a new animation frame.
This effectively means adding the concept of time to our application. You're free to play with different values, but let's start with a new frame every 10 milliseconds:
(def interval js/Rx.Observable.interval) (def time (interval 10))
As RxJS is a JavaScript library, we need to use ClojureScript's interoperability to call its functions. For convenience, we bind the interval
function of RxJS to a local var. We will use this approach throughout this book when appropriate.
Next, we create an infinite stream of numbers—starting at 0—that will have a new element every 10 milliseconds. Let's make sure this is working as expected:
(-> time (.take 5) (.subscribe (fn [n] (.log js/console n)))) ;; 0 ;; 1 ;; 2 ;; 3 ;; 4
Tip
I use the term stream very loosely here. It will be defined more precisely later in this book.
Remember time is infinite, so we use .take
in order to avoid indefinitely printing out numbers to the console.
Our next step is to calculate the 2D coordinate representing a segment of the sine wave we can draw. This will be given by the following functions:
(defn deg-to-rad [n] (* (/ Math/PI 180) n)) (defn sine-coord [x] (let [sin (Math/sin (deg-to-rad x)) y (- 100 (* sin 90))] {:x x :y y :sin sin}))
The sine-coord
function takes an x
point of our 2D Canvas and calculates the y
point based on the sine of x
. The constants 100
and 90
simply control how tall and sharp the slope should be. As an example, try calculating the sine coordinate when x
is 50:
(.log js/console (str (sine-coord 50))) ;;{:x 50, :y 31.05600011929198, :sin 0.766044443118978}
We will be using time
as the source for the values of x
. Creating the sine wave now is only a matter of combining both time
and sine-coord
:
(def sine-wave (.map time sine-coord))
Just like time
, sine-wave
is an infinite stream. The difference is that instead of just integers, we will now have the x
and y
coordinates of our sine wave, as demonstrated in the following:
(-> sine-wave (.take 5) (.subscribe (fn [xysin] (.log js/console (str xysin))))) ;; {:x 0, :y 100, :sin 0} ;; {:x 1, :y 98.42928342064448, :sin 0.01745240643728351} ;; {:x 2, :y 96.85904529677491, :sin 0.03489949670250097} ;; {:x 3, :y 95.28976393813505, :sin 0.052335956242943835} ;; {:x 4, :y 93.72191736302872, :sin 0.0697564737441253}
This brings us to the original code snippet which piqued our interest, alongside a function to perform the actual drawing:
(defn fill-rect [x y colour] (set! (.-fillStyle ctx) colour) (.fillRect ctx x y 2 2)) (-> sine-wave (.take 600) (.subscribe (fn [{:keys [x y]}] (fill-rect x y "orange"))))
As this point, we can save the file again and watch as the sine wave we have just created gracefully appears on the screen.
More colors
One of the points this example sets out to illustrate is how thinking in terms of very simple abstractions and then building more complex ones on top of them make for code that is simpler to maintain and easier to modify.
As such, we will now update our animation to draw the sine wave in different colors. In this case, we would like to draw the wave in red if the sine of x
is negative and blue otherwise.
We already have the sine value coming through the sine-wave
stream, so all we need to do is to transform this stream into one that will give us the colors according to the preceding criteria:
(def colour (.map sine-wave (fn [{:keys [sin]}] (if (< sin 0) "red" "blue"))))
The next step is to add the new stream into the main drawing loop—remember to comment the previous one so that we don't end up with multiple waves being drawn at the same time:
(-> (.zip sine-wave colour #(vector % %2)) (.take 600) (.subscribe (fn [[{:keys [x y]} colour]] (fill-rect x y colour))))
Once we save the file, we should see a new sine wave alternating between red and blue as the sine of x
oscillates from –1 to 1.
Making it reactive
As fun as this has been so far, the animation we have created isn't really reactive. Sure, it does react to time itself, but that is the very nature of animation. As we will later see, Reactive Programming is so called because programs react to external inputs such as mouse or network events.
We will, therefore, update the animation so that the user is in control of when the color switch occurs: the wave will start red and switch to blue when the user clicks anywhere within the canvas area. Further clicks will simply alternate between red and blue.
We start by creating infinite—as per the definition of time
—streams for our color primitives as follows:
(def red (.map time (fn [_] "red"))) (def blue (.map time (fn [_] "blue")))
On their own, red
and blue
aren't that interesting as their values don't change. We can think of them as constant streams. They become a lot more interesting when combined with another infinite stream that cycles between them based on user input:
(def concat js/Rx.Observable.concat) (def defer js/Rx.Observable.defer) (def from-event js/Rx.Observable.fromEvent) (def mouse-click (from-event canvas "click")) (def cycle-colour (concat (.takeUntil red mouse-click) (defer #(concat (.takeUntil blue mouse-click) cycle-colour))))
This is our most complex update so far. If you look closely, you will also notice that cycle-colour
is a recursive stream; that is, it is defined in terms of itself.
When we first saw code of this nature, we took a leap of faith in trying to understand what it does. After a quick read, however, we realized that cycle-colour
follows closely how we might have talked about the problem: we will use red until a mouse click occurs, after which we will use blue until another mouse click occurs. Then, we start the recursion.
The change to our animation loop is minimal:
(-> (.zip sine-wave cycle-colour #(vector % %2)) (.take 600) (.subscribe (fn [[{:keys [x y]} colour]] (fill-rect x y colour))))
The purpose of this book is to help you develop the instinct required to model problems in the way demonstrated here. After each chapter, more and more of this example will make sense. Additionally, a number of frameworks will be used both in ClojureScript and Clojure to give you a wide range of tools to choose from.
Before we move on to that, we must take a little detour and understand how we got here.
Exercise 1.1
Modify the previous example in such a way that the sine wave is drawn using all rainbow colors. The drawing loop should look like the following:
(-> (.zip sine-wave rainbow-colours #(vector % %2)) (.take 600) (.subscribe (fn [[{:keys [x y]} colour]] (fill-rect x y colour))))
Your task is to implement the rainbow-colours
stream. As everything up until now has been very light on explanations, you might choose to come back to this exercise later, once we have covered more about CES.
The repeat
, scan
, and flatMap
functions may be useful in solving this exercise. Be sure to consult RxJs' API at https://github.com/Reactive-Extensions/RxJS/blob/master/doc/libraries/rx.complete.md.