When designing interactive applications such as desktop Graphical User Interfaces (GUIs), we are essentially using an object-oriented approach to Reactive Programming. We will build a simple calculator application to demonstrate this style.
Clojure isn't an object-oriented language, but we will be interacting with parts of the Java API to build user interfaces that were developed in an OO paradigm, hence the title of this section.
Let's start by creating a new Leiningen project from the command line:
lein new calculator
This will create a directory called calculator in the current folder. Next, open the project.clj file in your favorite text editor and add a dependency on seesaw, a Clojure library for working with Java Swing:
(defproject calculator "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.8.0"]
[seesaw "1.5.0"]])
At the time of writing this book, the latest seesaw version that's available is 1.5.0.
Next, in the src/calculator/core.clj file, we'll start by requiring the seesaw library and creating the visual components we'll be using:
(ns calculator.core
(:require [seesaw.core :refer :all]))
(native!)
(def main-frame (frame :title "Calculator" :on-close :exit))
(def field-x (text "1"))
(def field-y (text "2"))
(def result-label (label "Type numbers in the boxes to add them up!"))
The preceding snippet creates a window with the title Calculator, which ends the program when closed. We also created two text input fields, field-x and field-y, as well as a label that will be used to display the results, aptly named result-label.
We would like the label to be updated automatically as soon as a user types a new number into any of the input fields. The following code does exactly that:
(defn update-sum [e]
(try
(text! result-label
(str "Sum is " (+ (Integer/parseInt (text field-x))
(Integer/parseInt (text field-y)))))
(catch Exception e
(println "Error parsing input."))))
(listen field-x :key-released update-sum)
(listen field-y :key-released update-sum)
The first function, update-sum, is our event handler. It sets the text of result-label to the sum of the values in field-x and field-y. We use try/catch here as a really basic way to handle errors, since the key that's being pressed might not have been a number.
We then add the event handler to the :key-released event of both input fields.
In real applications, we never want a catch block such as the previous one. This is considered bad style, and the catch block should do something more useful, such as logging an exception, firing a notification, or resuming the application if possible.
We are almost done. All we need to do now is add the components we have created so far to our main-frame and, finally, display it as follows:
(config! main-frame :content
(border-panel
:north (horizontal-panel :items [field-x field-y])
:center result-label
:border 5))
(defn -main [& args]
(-> main-frame pack! show!))
Now, we can save the file and run the program from the command line in the project's root directory:
lein run -m calculator.core
You should see something like the following screenshot:
Experiment by typing some numbers into either or both text input fields and watch how the value of the label changes automatically, displaying the sum of both numbers.
Congratulations! You have just created your first reactive application!
As alluded to previously, this application is reactive because the value of the result label reacts to user input and is updated automatically. However, this isn't the whole story—it lacks in composability and requires us to specify the how, not the what, of what we're trying to achieve.
As familiar as this style of programming may be, making applications reactive this way isn't always ideal.
Given the previous discussions, we noticed that we still had to be fairly explicit in setting up the relationships between the various components, as evidenced by having to write a custom handler and binding it to both input fields.
As we will see throughout the rest of this book, there is a much better way to handle similar scenarios.