Skip to content

Latest commit

 

History

History
324 lines (227 loc) · 8.23 KB

part-4.org

File metadata and controls

324 lines (227 loc) · 8.23 KB

Clojure School

Homework

What’s up tonight?

  • Pizza/beer
  • Implementing a multiplayer snake game
  • Pub

The Expression Problem

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.

A generalisation - OOP

Object Oriented languages make it relatively easy to add new types

List.addList.getList.clearList.size
ArrayList
LinkedList
Stack
Vector
New Class

A generalisation - FP

Functional languages make it relatively easy to add new behaviours

conjnthemptycountNew function
list
vector
map
set

Expressivity

Being able to add new behaviours to existing types and new types to existing behaviours

Polymorphism in Clojure

Polymorphism in functional languages is achieved differently to polymorphism in OOP languages.

Clojure achieves polymorphism through Protocols and Multimethods

Protocols

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)

‘Implementing’ protocols

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}))

Extending to existing types

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))

Or create anonymous types

We create anonymous types with reify:

(let [my-value (+ 4 5)]
  (reify PossiblyBlank
    (blank? [_] false)
    (value [_] my-value)))

Multimethods

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’.

Multimethods

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")))

Multiplayer Snake

You’ll need to clone

https://github.com/likely/snake.git

Of Widgets and Models

Your first challenge:

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’)

Laziness

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))

Testing your ‘component’

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))

Your second challenge: wiring up the game state

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...)

Your third challenge: sending the commands to the model

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’s pipe:
(a/pipe src-ch dest-ch)

Server-client communication with WebSockets

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

Client-side Chord:

(: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)))))

Your fourth challenge: rendering the state from the server

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

Your final challenge: send your commands to the server

  • 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.

The finished game!

You should be able to open up multiple browser windows (or share your IP+port with a neighbour) to play multiplayer snake!

Congratulations!

What didn’t we cover?

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!

Cool Libraries

  • Cascalog
  • Overtone
  • Quill
  • core.typed/Schema
  • core.logic
  • Datomic (database)
  • clj-time (wrapping Joda time)

Further learning

Clojure Dojos

See the London Clojurians mailing list or follow @ldnclj on Twitter.

They’re normally the second Monday or the last Tuesday of the month.

Thank You!