Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
519 lines (383 sloc) 12.5 KB

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

./images/clojure-time.png

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:

SynchronousAsynchronous
UncoordinatedAtom
Coordinated

Clojure’s other concurrency primitives:

SynchronousAsynchronous
UncoordinatedAtomAgent
CoordinatedRefn/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))))