https://github.com/likely/weather
https://github.com/bbatsov/clojure-style-guide
Definitely bedtime reading
- Clojure’s Web Stack
- Managing State in Clojure
- Ring: provides web server capability
- Compojure: provides a dsl for creating web handlers
- Hiccup: provides html templating
Concepts: handlers, routes, requests, responses, middleware
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}
lein ring server-headless
You will probably see a web server start on http://localhost:3000
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.
;; 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!"})))
(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}
The string “Hello World” is converted into a response map automatically.
(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!
Update the handler to return a web page at:
http://localhost:3000/ping
That returns “pong”.
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/*" [] ...)
: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)
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.
(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?
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
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
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)
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?!
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.
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)
- 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
Synchronous | Asynchronous | |
---|---|---|
Uncoordinated | Atom | |
Coordinated |
Synchronous | Asynchronous | |
---|---|---|
Uncoordinated | Atom | Agent |
Coordinated | Ref | n/a |
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 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
(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
Add a visitor counter to the bottom of your web page that increments for each visit.
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)))
(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))))
(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))))
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…
What we’ve covered:
- Ring, Compojure, Hiccup
- Handlers, Routes, Requests, Responses, Middleware
- Managing state with Atoms, Refs, Agents
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))))