- Pizza/beer
- Implementing a multiplayer snake game
- Pub
The Expression Problem refers to the problem of making a language extensible.
Software manipulates data types using operations.
Sometimes, we want to add new operations, and have them work on existing data types.
Sometimes, we want to add new data types, which will work with existing operations.
Object Oriented languages make it relatively easy to add new types
List.add | List.get | List.clear | List.size | |
---|---|---|---|---|
ArrayList | ||||
LinkedList | ||||
Stack | ||||
Vector | ||||
New Class | ✓ | ✓ | ✓ | ✓ |
Functional languages make it relatively easy to add new behaviours
conj | nth | empty | count | New function | |
list | ✓ | ||||
vector | ✓ | ||||
map | ✓ | ||||
set | ✓ |
Being able to add new behaviours to existing types and new types to existing behaviours
Polymorphism in functional languages is achieved differently to polymorphism in OOP languages.
Clojure achieves polymorphism through Protocols and Multimethods
Protocols define of a set of functions that implementers should
implement defined using defprotocol
:
(defprotocol PossiblyBlank
(blank? [_])
(value [_]))
Protocols are similar in nature to Java’s interfaces, but without some of their drawbacks:
- Protocols can be retroactively added to classes (and nil!)
- Protocols are completely independent of each other (no inheritance hierarchy)
Protocols can be implemented by ‘records’ - a Clojure map with ‘type’ metadata (used for the dynamic dispatch)
(defrecord MyRecord [a-key b-key]
PossiblyBlank
(blank? [_] (not (or a-key b-key))
(value [this] this)))
(def record (MyRecord. :a :b))
(def record-2
(map->MyRecord {:a-key :a
:b-key :b}))
We can also extend existing types (and nil) with extend-protocol
:
(extend-protocol PossiblyBlank
String
(blank? [s] (not (zero? (count s))))
(value [s] s)
nil
(blank? [_] true)
(value [_] nil))
We create anonymous types with reify
:
(let [my-value (+ 4 5)]
(reify PossiblyBlank
(blank? [_] false)
(value [_] my-value)))
We don’t have to dispatch on type - we can dispatch on an arbitrary function:
(defmulti apply-command (fn [position command] command))
(defmethod apply-command :move-up [position _]
(update-in position [:y] dec))
(defmethod apply-command :move-right [position _]
(update-in position [:x] inc))
We can dispatch on type, by making the dispatch function ’type
’.
The implementations of a multimethod do not have to reside with the
original defmulti
definition.
(defmethod print-method PossiblyBlank [this w]
(.write w (if (blank? this)
(pr-str (value this))
"blank")))
You’ll need to clone
https://github.com/likely/snake.git
Define and implement the GameBoardComponent protocol in /src/cljs/snake/cljs/board_widget.cljs
It’ll need:
- a way to render a ‘snake’ (given a list of cells)
- a way to render an ‘apple’ (given x-y co-ords)
- a way to clear the canvas for the next frame (hint - ‘clearRect’)
Recap: map is lazy - unless you evaluate the results it won’t do any work!
We force this by using dorun
(if we don’t care about the return value) or doall
(if we do):
(dorun (map render-cell! cells))
Or we can use doseq
(an eager equivalent of for
):
(doseq [cell cells]
(render-cell! cell))
You can test your component by injecting ‘test code’ into the
make-board-widget
function:
(let [board ...]
(render-snake! board [[7 4] [7 5] [7 6]])
(board->node board))
Implement the watch-game!
function.
Every time the !game
atom changes, we’ll need to clear the board, and
draw the new state.
An example !game
state is in /src/cljs/snake/cljs/multiplayer_model.cljs
Again, test by injecting code into make-board-widget
:
(reset! !game ...test-game-state...)
Implement the bind-commands!
function
- Add a function to the protocol which returns a channel of events
- Implement it!
- In
bind-commands!
, you can then pipe these events straight to the model channel with core.async’spipe
:
(a/pipe src-ch dest-ch)
WebSockets are persistent channels through which data can be sent in both directions between the browser and the server.
Chord is a library that turns WebSockets into core.async channels
Full documentation at:
https://github.com/james-henderson/chord.git
(:require [chord.client :refer [ws-ch]]
[cljs.core.async :refer [<! >! put! close!]])
(:require-macros [cljs.core.async.macros :refer [go]])
(go
(let [server-conn (<! (ws-ch "ws://localhost:3000/ws"))]
(>! server-conn "Hello server from client!")))
Messages that come from the server are received as a map with a :message
key:
(go
(let [server-conn (<! (ws-ch "ws://localhost:3000/ws"))]
(js/console.log "Got message from server:"
(:message (<! server-conn)))))
In /src/cljs/snake/cljs/multiplayer_model.cljs
:
- Implement
watch-state!
. - You might want to start by
js/console.log
‘ging everything you get back from the server! - When it all works, you should see a small snake going from right->left
- Implement
send-commands!
. - The websocket expects commands as strings - you can
pr-str
the keywords. - The server expects one of
#{:up :down :left :right}
. - Don’t worry about player-id - this is handled on the server.
You should be able to open up multiple browser windows (or share your IP+port with a neighbour) to play multiplayer snake!
Congratulations!
It was tough to fit ‘all of Clojure’ into four 2-hour sessions! We chose material based on what we use most often in our day-to-day work.
As a result, we haven’t covered:
- Java Interop
- File I/O
- Resources / Resource scoping
- Common formatting/parsing libraries
- Deployment
If you have any questions about any of the above, feel free to ask, either in the pub, or on the mailing list!
- Cascalog
- Overtone
- Quill
- core.typed/Schema
- core.logic
- Datomic (database)
- clj-time (wrapping Joda time)
- http://clojure-doc.org/
- http://www.clojure-toolbox.com/
- http://www.braveclojure.com/
- http://clojure.org/cheatsheet
- Google Groups
- clojure
- london-clojurians
- ClojureScript
- Talks at Skills Matter on first Tuesday of the month
See the London Clojurians mailing list or follow @ldnclj on Twitter.
They’re normally the second Monday or the last Tuesday of the month.