Clojure School
One I prepared earlier
https://github.com/likely/weather
Clojure Style guide
https://github.com/bbatsov/clojure-style-guide
Definitely bedtime reading
Today’s lesson
- Clojure’s Web Stack
- Managing State in Clojure
Clojure’s web stack
- Ring: provides web server capability
- Compojure: provides a dsl for creating web handlers
- Hiccup: provides html templating
Concepts: handlers, routes, requests, responses, middleware
A new Compojure application
lein new compojure <name>
Look in your new project’s ./project.clj to see the dependencies.
Note the ring key in the project map:
:ring {:handler clojureschool.handler/app}
Boot up the web server
lein ring server-headless
You will probably see a web server start on http://localhost:3000
The template web application
Open ./src/<name>/handler.clj
(ns clojureschool.handler
(:use compojure.core)
(:require [compojure.handler :as handler]
[compojure.route :as route]))
(defroutes app-routes
(GET "/" [] "Hello World")
(route/resources "/")
(route/not-found "Not Found"))
(def app
(handler/site app-routes))
Try changing “Hello World”, code should be reloaded on browser refresh.
A generic response - a plain old map
;; The routes are just a function that takes a request
;; and returns a response
(def app
(handler/site (fn [request]
{:status 200
:headers {"Content-Type" "text/plain"}
:body "Hello World!"})))
What is a request?
(def app
(handler/site (fn [request]
{:status 200
:headers {"Content-Type" "text/plain"}
:body (str request)})))
{:ssl-client-cert nil, :remote-addr "0:0:0:0:0:0:0:1%0", :scheme :http, :query-params {}, :session {}, :form-params {}, :multipart-params {}, :request-method :get, :query-string nil, :content-type nil, :cookies {}, :uri "/", :session/key nil, :server-name "localhost", :params {}, :headers {"accept-encoding" "gzip,deflate,sdch", "cache-control" "max-age=0", "connection" "keep-alive", "user-agent" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36", "accept-language" "en-US,en;q=0.8,fr-FR;q=0.6,fr;q=0.4", "accept" "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "host" "localhost:3001"}, :content-length nil, :server-port 3001, :character-encoding nil, :body #<HttpInput org.eclipse.jetty.server.HttpInput@13552393>, :flash nil}
Requests and responses are just Clojure maps
The string “Hello World” is converted into a response map automatically.
Compojure
(ns clojureschool.handler
(:use compojure.core)
(:require [compojure.handler :as handler]
[compojure.route :as route]))
(defroutes app-routes
(GET "/" [] "Hello World")
(route/resources "/")
(route/not-found "Not Found"))
(def app
(handler/site app-routes))
Compojure provides a nice DSL for mapping requests to responses. Don’t worry about how it works yet!
Exercise
Update the handler to return a web page at:
http://localhost:3000/ping
That returns “pong”.
Routes
Syntax familiar to anyone with a rails background
;; Named params
(GET "/:a/:b" [a b] ...)
;; Query parameters
;; e.g. /something?a=1&b=2
(GET "/something" [a b] ...)
;; Catch-all
(GET "/blah/*" [] ...)
Web templating with Hiccup
:dependencies [[hiccup "1.0.4"] ...]
(ns webapp.core
(:require [hiccup.core :refer :all]
[hiccup.page :refer [html5]]))
(def index-view
(html5
[:head [:title "Hiccup Website"]]
[:body [:h1 "Title goes here"]
[:ul (for [x (range 10)]
[:li (str "Item " x)])]]))
(GET "/" [] index-view)
Exercise
Update the handler to return a web page at:
http://localhost:3000/list/[list_id]?n=21
That returns a page with “List: [list id]” in the title and a list of integers from 1 to n.
Adding a visitor counter!
(def counter 0)
(defn index-view [{:keys [counter]}]
(html5
[:head [:title "Web counter"]]
[:body [:h1 "Web counter"]
[:p (str "You're visitor number " (inc counter))]]))
How can we update the counter?
Thinking Functionally - Mutable State
A Clojure atom is a mutable pointer to an immutable data structure.
user=> (def counter (atom 0))
#'user/counter
We read the current value of the atom by dereferencing it:
user=> (deref counter)
0
user=> @counter
0
Updating an atom
We update the value of an atom by passing it a pure function, which takes the previous value of the atom and returns the next value:
user=> (swap! counter inc)
1
user=> @counter
1
Modelling Time in Clojure
Kicking off another thread
user=> (def a-future (future
(println "Starting massive calculation!")
(Thread/sleep 10000)
(println "Finished massive calculation!")
42))
#'user/a-future
Starting massive calculation!
user=> a-future
#
user=> @a-future
Finished massive calculation!
42
See also: realized?, future-cancel, future-cancelled?, deref (multiple arities)
Why bother with atoms?
Atoms support sharing state between multiple threads, without many of the common concurrency pitfalls:
- No (user) locking
- No deadlocks
- ‘ACI’ (no durability - it’s in memory!)
But how?!
Software Transactional Memory
The function you pass to ‘swap!’ is run inside a transaction.
If multiple updates are made simultaneously to the same atom, the Clojure STM system (transparently) aborts and retries the updates so that anyone reading the atoms sees consistent values.
I/O in transactions
No!
The transaction might be aborted and retried - it’s very difficult to abort I/O and (most of the time) unwise to retry it!
(let [counter (atom 0)]
(dotimes [_ 10]
(future
(swap! counter
(fn [old-counter]
(print old-counter)
(inc old-counter))))))
=> 00000000001111111122222213334332554366576879
We have ways around this (to be covered later)
Exercise
- Start a thread (future) to sum all the numbers to one hundred million
- Create a function which appends the numbers 1-10 to a vector in an atom (hint: dotimes)
- Run the function across 10 threads simultaneously and observe the output
- Extra credit: write a function that confirms you have ten of each number
Clojure’s other concurrency primitives:
Synchronous | Asynchronous | |
---|---|---|
Uncoordinated | Atom | |
Coordinated |
Clojure’s other concurrency primitives:
Synchronous | Asynchronous | |
---|---|---|
Uncoordinated | Atom | Agent |
Coordinated | Ref | n/a |
Refs - co-ordinated updates
Refs are also pointers to values, but updates to multiple refs are co-ordinated.
Transactions must be explictly demarcated with a (dosync …) block:
(def account-a (ref 2341))
(def account-b (ref 4123))
(def transaction-log (ref []))
(dosync
(alter account-a + 100)
(alter account-b - 100)
(alter transaction-log conj {:from :b, :to :a, :amount 100}))
Agents - asynchronous updates
Agents are also pointers to values, but updates are asynchronous:
(def log-messages (agent []))
(send-off log-messages conj "Something happened!")
Actions sent to an individual agent are queued, not re-tried - only one action runs on any given agent at any time.
So they’re suitable for I/O!
See also: send
I/O in transactions - revisited:
(def log-agent (agent nil))
(def my-counter (atom 0))
(dotimes [_ 10]
(swap! my-counter
(fn [old-counter]
(let [new-counter (inc old-counter)]
(send-off log-agent (fn [_] (println "Counter is now:" new-counter)))
new-counter))))
- Sent/Sent-off actions are only queued when the transaction is successful
Exercise
Add a visitor counter to the bottom of your web page that increments for each visit.
Middleware
Middleware are functions which are chained together and adapt requests and/or responses
(defn wrap-log-request [handler]
(fn [req]
(println req)
(handler req))
(def app
(handler/site (wrap-log-request app-routes)))
Middleware can be chained
(defn wrap-log-request [handler]
(fn [request]
(println request)
(handler request))
(defn wrap-log-response [handler]
(fn [request]
(let [response (handler request)]
(println response)
response)))
(def app
(handler/site (wrap-log-response (wrap-log-request app-routes))))
Threading macros
(def app
(-> app-routes
wrap-request-log
wrap-response-log
handler/site))
Compare with:
(def app
(handler/site (wrap-response-log (wrap-request-log app-routes))))
Useful Middleware
e.g for JSON response
:dependencies [[ring-middleware-format "0.3.1"] ...]
(ns clojureschool.handler
(:require [ring.middleware.format :refer [wrap-restful-format]]))
(def app
(-> app-routes
(wrap-restful-format :formats [:json-kw :edn])
handler/site))
This instructs ring to convert Clojure data structures to their JSON equivalent, or as edn format if the Accept header requests it.
Also middleware for cookies, authentication, rate limiting, compression etc…
That’s it!
What we’ve covered:
- Ring, Compojure, Hiccup
- Handlers, Routes, Requests, Responses, Middleware
- Managing state with Atoms, Refs, Agents
Further exercises
Add a CSS stylesheet to your hiccup template and change the font color (hint: include-css)
Write some middleware that returns a 401 status code unless ?token=123 is supplied in the URL
Adapt your counter update to print a message to the console every 10th visitor (hint: I/O and agents)
;; Re-write the below using -> threading macro
(/ (* (+ 10 2) 5) (* 2 5))
;; Re-write the below using ->> threading macro
(* 10 (apply + (map inc (range 10))))