Hands-On Reactive Programming with Clojure
上QQ阅读APP看书,第一时间看更新

Building a stock market monitoring application

Our stock market program will consist of three main components:

  • A function simulating an external service from which we can query the current price—this would likely be a network call in a real setting
  • A scheduler that polls the preceding function at a predefined interval
  • A display function that's responsible for updating the screen

We'll start by creating a new Leiningen project, where the source code for our application will live. Type the following on the command line and then switch into the newly created directory:

lein new stock-market-monitor
cd stock-market-monitor  

As we'll be building a GUI for this application, go ahead and add a dependency on seesaw to the dependencies section of your project.clj:

[seesaw "1.5.0"] 

Next, create a src/stock_market_monitor/core.clj file in your favorite editor. Let's create and configure our application's UI components:

(ns stock-market-monitor.core 
  (:require [seesaw.core :refer :all]) 
  (:import (java.util.concurrent ScheduledThreadPoolExecutor 
                                 TimeUnit))) 
 
(native!) 
 
(def main-frame (frame :title "Stock price monitor" 
                       :width 200 :height 100 
                       :on-close :exit)) 
 
(def price-label       (label "Price: -"))     
 
(config! main-frame :content price-label) 

As you can see, the UI is fairly simple. It consists of a single label that will display a company's share price. We also imported two Java classes, ScheduledThreadPoolExecutor and TimeUnit, which we will use shortly.

The next thing we need is our polling machinery so that we can invoke the pricing service on a given schedule. We'll implement this via a thread pool so as not to block the main thread:

(def pool (atom nil)) 
 
(defn init-scheduler [num-threads] 
  (reset! pool  (ScheduledThreadPoolExecutor. num-threads))) 
(defn run-every [pool millis f] 
  (.scheduleWithFixedDelay pool 
                           f 
                           0 millis TimeUnit/MILLISECONDS)) 
 
(defn shutdown [pool] 
  (println "Shutting down scheduler...") 
  (.shutdown pool)) 
User interface SDKs such as Swing have the concept of a main—or UI—thread. This is the thread that's used by the SDK to render the UI components to the screen. As such, if we have blocked,   or even simply slow-running, operations executing in this thread, the user's experience will be severely affected, hence the use of a thread pool to offload expensive function calls.

The init-scheduler function creates ScheduledThreadPoolExecutor with the given number of threads. That's the thread pool in which our periodic function will run. The run-every function schedules a function, f, in the given pool to run at the interval specified by millis. Finally, shutdown is a function that will be called on program termination and thus will shut down the thread pool gracefully.

The rest of the program puts all of these parts together:

(defn share-price [company-code] 
  (Thread/sleep 200) 
  (rand-int 1000)) 
 
 
(defn -main [& args] 
  (show! main-frame) 
  (.addShutdownHook (Runtime/getRuntime) 
                    (Thread. #(shutdown @pool))) 
  (init-scheduler 1) 
  (run-every @pool 500 
             #(->> (str "Price: " (share-price "XYZ")) 
                   (text! price-label) 
                   invoke-now))) 

The share-price function sleeps for 200 milliseconds to simulate network latency and returns a random integer between 0 and 1,000, representing the stock's price.

The second line of our -main function adds a shutdown hook to the runtime. This allows our program to intercept termination, such as pressing Ctrl + C in a Terminal window, and gives us the opportunity to shut down the thread pool.

The ScheduledThreadPoolExecutor pool creates non-daemon threads by default. A program cannot terminate if there are any non-daemon threads alive in addition to the program's main thread. This is why the shutdown hook is necessary.

Next, we initialize the scheduler with a single thread and schedule a function to be executed every 500 milliseconds. This function asks the share-price function for XYZ's current price and updates the label.

Desktop applications require all rendering to be done in the UI thread. However, our periodic function runs on a separate thread and needs to update the price label. This is why we use invoke-now, which is a seesaw function that schedules its body to be executed in the UI thread as soon as possible.

Let's run the program by typing the following command in the project's root directory:

lein trampoline run -m stock-market-monitor.core  
Trampolining tells Leiningen not to nest our program's JVM within its own, thus freeing us to handle uses of Ctrl + C ourselves through shutdown hooks.

A window like the one shown in the following screenshot will be displayed, with the values on it being updated as per the schedule that we implemented earlier:

This is a fine solution. The code is relatively straightforward and satisfies our original requirements. However, if we look at the big picture, there is a fair bit of noise in our program. Most of its lines of code are dealing with creating and managing a thread pool, which, while necessary, isn't central to the problem we're solving—it's an implementation detail.

We'll keep things as they are for the moment and add a new requirement: rolling averages.