Skip to content

Latest commit

 

History

History
571 lines (394 loc) · 12.2 KB

part-3.org

File metadata and controls

571 lines (394 loc) · 12.2 KB

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.