Clojure School
One I prepared earlier
git clone git://github.com/likely/sketchy.git
cd sketchy
lein start-server
# To connect to the REPL:
lein repl :connect 7888
A quick refresher
- Requests and responses are just maps
- Handlers are functions for turning requests into responses
- Atoms allow state to be mutated with pure functions
- Changes are atomic and thread safe
- We proved this by starting threads with future
Today’s lesson
- How does defroutes help turn requests into responses?
- What are other ways of dealing with concurrency?
- Can we use these ideas to make an interactive, multi-person app?
We’ll be covering
- Macros
- core.async
- ClojureScript
Caveat: these are big topics!
We could easily spend a day on each one.
We’ll give you an understanding of the basics, and hopefully the hunger to find out more.
In LISPs, (= Code Data)
(inc 0) is simply a list of two elements - the symbol ‘inc’, and the value ‘0’.
The REPL evaluates lists by treating the first argument as a function, and the rest as arguments to that function:
user=> (inc 0)
1
We can ask the REPL not to eval a form by quoting it:
user=> '(inc 0)
(inc 0)
This is why you needed to quote ‘requires’ in the first week - to stop the REPL trying to evaluate it!
user=> (require '[clj-http.core :reload :all])
nil
Exercise
Try this on a ‘defn’ from last week - what does it return (literally)?
user=> '(defn your-fn [your-params] (do-something ...))
???
Eval is the opposite of quote
user=> (eval '(inc 0))
1
This is what the REPL is doing to your code!
Macros
A macro is just a function that receives its arguments unevaluated at compile time.
Because the unevaluated arguments are code, macros can operate on the code as if it were data.
Macros we’ve already used
‘when’, ‘and’ and ‘or’ are macros - because they need to control the execution order of their parameters.
If ‘when’ were a function, it could be implemented thus:
(defn bad-when [test & then]
(if test
then
nil))
But look what happens when the test is false:
user=> (bad-when (= 1 0)
(println "uh oh!"))
uh oh!
nil
Because functions’ parameters are evaluated before the function is called, we will always evaluate the ‘then’ block!
This will not do!
So ‘when’ must be a macro.
Macroexpand
You can use the function macroexpand and macroexpand-1 to see the effect of macros (indentation mine):
user=> (macroexpand-1 '(when (= 1 0)
(println "uh oh!")
(println "this isn't good!")))
(if (= 1 0)
(do
(println "uh oh!")
(println "this isn't good!")))
Exercise
What does the following expand to?
(and (= 1 0) (println "Hello World"))
Other macros in Clojure.core
and, or, when, while, for, cond, ->, ->>
-> and ->> are the threading macros - they restructure heavily nested code to make it more legible:
Threading macros
(let [my-map {:b 2
:c {:d 3
:e 5}}]
(vals (update-in (assoc my-map :b 3) [:c :e] inc)))
;; could be better written as
(-> {:b 2
:c {:d 3
:e 5}}
(assoc :b 3)
(update-in [:c :e] inc)
vals)
(let [forecast-days (forecast "london,uk" {:cnt 10})]
(count (remove cloudy? forecast-days)))
;; could be written as:
(->> (forecast "london,uk" {:cnt 10})
(remove cloudy?)
count)
Exercise
;; 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))))
Macro magic: core.async
An implementation of Communicating Sequential Processes (CSP).
CSP is an old idea, but basis of concurrency in languages such as Google’s Go.
The processes are more lightweight than threads, suitable for highly parallel web apps.
core.async
Channels are the fundamental building blocks of core.async:
user=> (require '[clojure.core.async :refer [go go-loop >! <! put! chan])
nil
user=> (chan)
#<ManyToManyChannel clojure.core.async.impl.channels.ManyToManyChannel@30804b08>
Normally, all operations on channels are blocking, but (so that we can test the examples on the upcoming slides) we can asynchronously put a value on a channel with ‘put!’:
(let [out (chan)]
(put! out 5)
out)
Go!
Operations in core.async are usually found within ‘go’ blocks.
A ‘go’ block transparently translates your beautiful synchronous code into the asynchronous callback-hell-style code you would have had to write otherwise.
‘Go’ blocks return a channel eventually containing a single value - the result of the ‘go’ block:
user=> (go 5)
#<ManyToManyChannel clojure.core.async.impl.channels.ManyToManyChannel@30804b08>
Taking from a channel
On the surface, <! (‘take’) appears to accept a channel and synchronously return a value from it.
<! must be used inside a go block.
user> (go
(let [value (<! (go 5))]
(println value)))
#<ManyToManyChannel clojure.core.async.impl.channels.ManyToManyChannel@5636e34>
5
It helps you avoid the following callback-hell: (pseudo-code)
(let [ch (go 5)]
(on-receive ch
(fn [msg]
(let [value msg]
(println value)))))
See also: >!
Exercise
Create a channel, ‘put!’ two values onto it, and use a ‘go’ block to print them in a single vector.
Exercise - Answer
Create a channel, ‘put!’ two values onto it, and use a ‘go’ block to print them in a single vector.
(let [ch (chan)]
(go
(let [val-1 (<! ch)
val-2 (<! ch)]
(prn [val-1 val-2])))
(put! ch 42)
(put! ch 64))
Synchronous
The semantics of CSP are that takes and puts are synchronous.
If you take, it will block until something puts.
It you put, it will block until something takes.
(let [c (chan)]
(go
(println "We are here")
(<! c)
(println "We won't get here")))
Channels
Channels allow goroutines to talk to each other.
;; Order doesn't matter
user=> (let [c (chan)]
(go
(>! c "A message"))
(go
(println "We are here")
(<! c)
(println "We made progress")))
We are here
#<ManyToManyChannel clojure.core.async.impl.channels.ManyToManyChannel@27d198c3>
We made progress
Looping
Clojure does have a ‘loop’ macro
(loop [a 1
b 10]
(when (< a b)
(prn {:a a :b b})
(recur (inc a) (dec b))))
Application to channels
Reading every message ever put on a channel:
(let [ch (chan)]
(go
(loop []
(when-let [msg (<! ch)]
(println "Message received! -" msg)
(recur)))))
‘go’ and ‘loop’ are used together so often, that there is a ‘go-loop’ short-hand:
(go-loop []
;; ...
(recur))
Exercise:
In your REPL:
- Define a channel with (def my-chan (chan))
- Set off a go-loop that asynchronously prints every message
- Put messages on your channel, and see them printed on the console!
ClojureScript
You can write client-side JavaScript in Clojure too!
Writing Clojurescript
Open the file
/src/cljs/sketchy/client.cljs
(ns sketchy.client
(:require [...]))
(defn greet [name]
(str "Hello " name))
Reload the page
And in your JavaScript console, type
sketchy.client.greet("World");
=> "Hello World"
Javascript Interop
Property access
;; obj.x becomes:
(.-x obj)
;; obj.x = y becomes:
(set! (.-x obj) y)
Calling functions
;; obj.call(something);
(.call obj something)
Access global javascript object
;; console.log("message") becomes:
(js/console.log "message")
Exercise
Create an event handler in ClojureScript that will print out the mouse coordinates.
// In javascript this would be
window.addEventListener("mousemove",
function(event) {
console.log(event);
});
Load JavaScript on page load
Add to bottom of ClojureScript:
(defn main []
;; do something interesting
)
(set! (.-onload js/window) main)
We’re calling a function called ‘main’ on page load.
Putting UI events on a channel
(defn events [el type]
(let [out (chan)]
(d/listen! el type
(fn [e]
(put! out e)))
out))
We’re outside a go block so we can’t use >!. We use the asynchronous version put!
Exercise
Inside your main function, create a go loop that prints out the mousemove events
Channels are like sequences
They have map and reduce too!
;; Takes a function and a source channel, and returns a channel which
;; contains the values produced by applying f to each value taken from
;; the source channel
(map< some-fn some-channel)
See also map>, reduce, filter>, filter<, remove>, remove<
Exercise
Adapt your main function to print out a vector containing only the x and y components of the mousemove event.
Reading from more than one channel
core.async has a function ‘alts!’, that takes multiple channels, and returns the first message from any of those channels:
(let [ch-1 (chan)
ch-2 (chan)]
(go-loop []
(let [[v c] (alts! [ch-1 ch-2])]
(if (= c ch-1)
(handle-message-from-ch-1 ...)
(handle-message-from-ch-2 ...))
(recur))))
Drawing into the canvas
(defn draw-point [canvas [x y]]
(let [context (.getContext canvas "2d")]
(.fillRect context x y 10 10)))
Exercise (+ homework)
Using the functions we have so far - finish the drawing app!
You’ll need:
- 3 event channels - listening to mousemove, mousedown and mouseup events respectively
- A go-loop inside your ‘main’ fn - to react to events on all 3 channels and draw points if the mouse is down (hint: ‘alts!’)
- Any problems - let us know!
What we covered
- Macros
- core.async
- ClojureScript
Thank You
Not covered:
Skip to the end
(defn main []
(let [canvas (sel1 :#canvas)
move (map< e->v (events canvas "mousemove"))
down (events canvas "mousedown")
up (events canvas "mouseup")]
(go-loop [draw-point? false]
(let [[v sc] (alts! [move down up])]
(condp = sc
down (recur true)
up (recur false)
move (do (when draw-point?
(js/console.log (pr-str v)))
(recur draw-point?)))))))
Websockets
Websockets are long-lived connections between the client and the server through which messages can be sent in both directions.
Creating a channel from a websocket
Uncomment includes at the top of the handler
(defn socket-handler [request]
(with-channel request ws-ch
(go-loop []
(let [{:keys [message]} (<! ws-ch)]
(>! ws-ch message)
(recur)))))
In your browser’s js console…
var socket = new WebSocket("ws://localhost:3000/ws");
socket.onmessage = function(event) { console.log(event.data); }
socket.send("Data to be sent");
You should see the data you sent echoed back.
Create a collaborative drawing app!
Adapt the server side keep an atom of clients, return events to all of them.