git clone git://github.com/likely/sketchy.git
cd sketchy
lein start-server
# To connect to the REPL:
lein repl :connect 7888
- 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
- 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?
- Macros
- core.async
- ClojureScript
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.
(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
Try this on a ‘defn’ from last week - what does it return (literally)?
user=> '(defn your-fn [your-params] (do-something ...))
???
user=> (eval '(inc 0))
1
This is what the REPL is doing to your code!
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.
‘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.
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!")))
What does the following expand to?
(and (= 1 0) (println "Hello World"))
and, or, when, while, for, cond, ->, ->>
-> and ->> are the threading macros - they restructure heavily nested code to make it more legible:
(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)
;; 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))))
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.
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)
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>
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: >!
Create a channel, ‘put!’ two values onto it, and use a ‘go’ block to print them in a single vector.
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))
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 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
Clojure does have a ‘loop’ macro
(loop [a 1
b 10]
(when (< a b)
(prn {:a a :b b})
(recur (inc a) (dec b))))
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))
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!
You can write client-side JavaScript in Clojure too!
Open the file
/src/cljs/sketchy/client.cljs
(ns sketchy.client
(:require [...]))
(defn greet [name]
(str "Hello " name))
And in your JavaScript console, type
sketchy.client.greet("World");
=> "Hello World"
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")
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);
});
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.
(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!
Inside your main function, create a go loop that prints out the mousemove events
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<
Adapt your main function to print out a vector containing only the x and y components of the mousemove event.
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))))
(defn draw-point [canvas [x y]]
(let [context (.getContext canvas "2d")]
(.fillRect context x y 10 10)))
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!
- Macros
- core.async
- ClojureScript
(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 are long-lived connections between the client and the server through which messages can be sent in both directions.
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)))))
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.
Adapt the server side keep an atom of clients, return events to all of them.