A taste of Reactive Programming
Before covering some history and background behind Reactive Programming and CES, I would like to open with a working, and hopefully compelling, example: an animation in which we draw a sine wave onto a web page.
The sine wave is simply a graphical representation of the sine function. It is a smooth, repetitive oscillation, and at the end of our animation, it will look like what's shown in the following screenshot:
This example will highlight how CES does the following:
- Urges us to think about what we would like to do as opposed to how
- Encourages small, specific abstractions that can be composed together
- Produces terse and maintainable code that is easy to change
The core of this program boils down to four lines of ClojureScript:
(-> app-time
(.pipe (rx-take 700)) (.subscribe (fn [{:keys [x y]}] (fill-rect x y) "orange"))))
Simply by looking at this code, it is impossible to determine precisely what it does. However, do take the time to read and imagine what it could do.
First, we have a variable called app-time, which represents a sequence of time. The next line gives us the intuition that app-time is some sort of collection-like abstraction: we use rx-take to retrieve 700 numbers from it.
Finally, we have to .subscribe to this collection by passing it a callback. This callback will be called for each item in the sine wave, finally drawing out the given sine coordinates using the fill-rect function.
This is quite a bit to take in for now, as we haven't seen any other code yet, but that was the point of this little exercise: even though we know nothing about the specifics of this example, we are able to develop an intuition of how it might work.
Let's see what else is necessary to make this snippet animate a sine wave on our screen.
This example is built in ClojureScript and uses HTML5 canvas for rendering and RxJS (see https://github.com/reactivex/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 in the next chapter. 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 figwheel (see https://github.com/bhauman/lein-figwheel), a Leiningen template for ClojureScript that gives us a sample working application that we can use as a skeleton.
To create our new project, head over to the command line and invoke Leiningen as follows:
lein new figwheel 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>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <link href="css/style.css" rel="stylesheet" type="text/css">
<script src="js/rxjs.umd.js" type="text/javascript"></script></head>
<body>
<div id="app"></div>
<script src="js/compiled/sin_wave.js" type="text/javascript"></script>
<canvas id="myCanvas" width="650" height="200" style="border:1px solid #d3d3d3;"></canvas>
</body>
</html>
This simply ensures that we import both our application code and RxJS. We haven't downloaded RxJS yet, so let's do that now. Browse to https://unpkg.com/rxjs/bundles/rxjs.umd.js and save this file to sin-wave/resources/public/js. The previous snippets also add an HTML5 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 on-js-reload[])
Finally, go back to the command line under the sin-wave folder and start up the following application:
lein figwheel Figwheel: Cutting some fruit, just a sec ...
Figwheel: Validating the configuration found in project.clj
Figwheel: Configuration Valid ;)
Figwheel: Starting server at http://0.0.0.0:3449
Figwheel: Watching build - dev
Figwheel: Cleaning build - dev
Compiling build :dev to "resources/public/js/compiled/sine_wave.js" from ["src"]...
Successfully compiled build :dev to "resources/public/js/compiled/sine_wave.js" in 16.12 seconds.
Figwheel: Starting CSS Watcher for paths ["resources/public/css"]
Launching ClojureScript REPL for build: dev
Figwheel Controls:
(stop-autobuild) ;; stops Figwheel autobuilder
(start-autobuild id ...) ;; starts autobuilder focused on optional ids
(switch-to-build id ...) ;; switches autobuilder to different build
(reset-autobuild) ;; stops, cleans, and starts autobuilder
(reload-config) ;; reloads build config and resets autobuild
(build-once id ...) ;; builds source one time
(clean-builds id ..) ;; deletes compiled cljs target files
(print-config id ...) ;; prints out build configurations
(fig-status) ;; displays current state of system
(figwheel.client/set-autoload false) ;; will turn autoloading off
(figwheel.client/set-repl-pprint false) ;; will turn pretty printing off
Switch REPL build focus:
:cljs/quit ;; allows you to switch REPL to another build
Docs: (doc function-name-here)
Exit: :cljs/quit
Results: Stored in vars *1, *2, *3, *e holds last exception object
Prompt will show when Figwheel connects to your application
[Rebel readline] Type :repl/help for online help info
ClojureScript 1.10.238
dev:cljs.user!{:conn 2}=>
Once the previous command finishes, the application will be available at http://localhost:3449/index.html, where you will find a blank, rectangular canvas. We are now ready to begin.
The main reason we are using the figwheel template for this example is that it performs hot-reloading of our application code via WebSockets. This means that 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 on 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 being 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 figwheel reloads our file. This is simple enough to do by adding the following snippet to our core namespace:
(defn canvas []
(.getElementById js/document "myCanvas"))
(defn ctx []
(.getContext (canvas) "2d"))