Clojure for the brave and the true¶
Parallel demons concurrency¶
- Reference cell: Read and write a shared state.
- Mutual exclusion: Example with writing a log with several processes, e.g. concatenate "ab" and "cd" in a file results in "acbd".
- Deadlock: On a table, every one has to take the left and right stick and the same time. However, there is only one stick per person. Leading to a deadlock.
Solutions to these are future (new thread), delay (like future but
executed only at deref time), promise (empty memory location shared
to all thread that will receive value only once with deliver). The
advantage is all the execution are cached. Note: deference is done
with either the defer function or the @ sign in front of the
variable.
References type concurrency epochal_time_model¶
-
atomare identities that can be set and shared by multiple threads. They use a set and compare algorithm, i.e. theswap!function modifies the value of the atom variable only if its values did not change between the beginning and the end of the transaction.swap!andreset!are the main function to work with atoms. -
watchare function with four arguments: a key (a keyword for identifying the process calling the watcher), a reference variable being watched, the old-state, and the new state.(defn f [key watched old-state new-state] nil)A watcher function is attached to a reference type (e.g an atom) with the
add-watchfunction having the following signature:(def counter (atom 0)) (add-watch counter :watching-counter watch-fn) -
Validators are functions that can check if new states are valid. They take as argument the atom and return a boolean. They are added to the atom as follow
(defn bigger-than-1 [x] (or (> x 1) (throw (IllegalStateException. "That's too small")))) (def account (atom 2 :validator bigger-than-1)) (swap! account inc) (swap! account - 10) ;; Throw an error -
reftype are the ACI in the ACID accronym (atomic, consistent and isolated) and use STM. It means that either the operations between two refs happened correctly, or the transaction is aborted.alteranddosyncare the key functions.- In a transaction (that is the body of
dosync), everyrefkeep their state to the transaction (invisible to outside threads) and when the transaction tries to commit, everyrefchecks if the value has been altered by other threads. - If any of them has been change, then none of the
refare updated, and the transaction restart with the new value and commits only when the initial states has not been /alter/ed by other processes.
commutealso allow to change the state of a ref. However, at transaction time, if ref states have been altered, only thecommutepart is run again with the new states, which might lead to inconsistent state, but increased performance.ensurefunction protects refs from being modified by other transaction. This is helpful, when a transaction must modify only one refs, but the other related refs must not be altered by other transaction. - In a transaction (that is the body of
-
varsare associations between symbols and objects.^:dynamicis a keyword indefto signal to clojure that a vars is dynamic. Varnames are enclosed around*(e.g.*user-email*) to show to other programmers that the variable is dynamic.bindingsis a dynamiclet. Dynamic vars are often use to name a resource that one ore more functions target.set!allows to change the state of the dynamic vars.alter-var-rootallows to rebind a immutable vars (which is unadvised), andwith-redefsallows to create local binding for testing. -
pmapand the followingppmapcan be used to execute parallel task:(defn ppmap "Partitioned pmap, for grouping map ops together to make parallel overehead worthwile" [grain-size f & colls] (apply concat (apply pmap (fn [& pgroups] (doall (apply map f pgroups))) (map (partial partition-all grain-size) colls))))
core.async and channels core_async¶
-
chancreates a channel. And channel communicate through messages. One can put and take message. Processes wait for completion of their message. Process: Wait and do nothing until successful completion of either put or take from a channel. After success of the operation, continue. -
goand their blocks (go blocks) runs separately on a concurrent thread.gocreates a process (i.e. its go block), which runs a pool of threads equal two plus the number of machines cores (avoiding the overhead of creating threads). Eachgo blockonly live until it reach the ends of its body. -
<!and<!!are the take function. It listen to the channel and wait until an another process puts a value in the channel which the take function returns.>!and>!!are the put function which always return true. It provides a message to a channel and wait until the message to be taken by another process before releasing resources. The number of!in the operation depends if one is inside a go block (one!) or not (two!). Blocking and parking waiting are key to understand the number of!. Parking wait allows a thread to handle several process (and this is only possible in a go block). When one of the process starts to wait, the thread put it aside and starts an another process until it starts to wait, and so on. Usepoll!andoffer!to have non blocking channel interactions in the REPL. -
Channel buffers are created as following:
(def buffer-size 2) (def channel-buffer (chan buffer-size))This means we can create 2 values without waiting for a response.
sliding-buffer(FIFO) anddropping-buffer(LIFO) can be used to discard channel message without blocking. -
close!closes channel. A closed channel does not accept any puts anymore and after all the values have been retrieved, the subsequent takes returnnil. -
alts!!lets us use the result of the first successful channel operation among a collection of channel operations. The elegant solution withalts!!is one can define a timeout(let [[message channel] (alts!! [c1 c2 (timout 20)])] ;; c1 and c2 are predefined channels. (println message))if the timeout is the first to finish than
messageisnil. Seealt!macro as well. -
Queues and pipelines (escaping the callback hell) are common patterns.
Abstraction and polymorphism¶
-
Multimethods
(defmulti method-name (fn [x] (:type x))) ;; or simplty :type, can be more complicated as well (defmethod method-name :hello [x] "Hello") (defmethod method-name :good-bye [x] "Good-bye") (defmethod method-name :default [x] "I don't know you") (method-name {:type :hello}) ; => Hello (method-name {:type :good-bye}) ; Good-bye (method-name {:type :what?}) ; => I don't know youOne can also create hierarchies with
deriveand namespace keywords. -
A protocol allows to make dispatch by the type of the first argument and it is a collection of polymorphic operations (unlike multimethod which is just one function). Methods from protocols can not have a
& restargument. Key functions aredefprotocol,extend-type,extend-protocol(for specifying for several type at once).- Caveat: methods from protocols are property of the namespace and not from the object.
-
Records are extension of
hash-map.(defrecord WereWolf [name title]) (WereWolf. "David" "Master") (->WereWolf "David" "Master") (map->WereWolf {:name "David" :title "Master"})On has to use the
:import:statement in thensmacro in order to import records. One can access field through the keyword or the dot.macro.(.name (WereWolf. "David" "Master")) ; "David" (:title (WereWolf. "David" "Master")) ; "Master"Any function on map works on record (although they do not retain their class if one
dissocorassocthem). Here is how one could extend a protocol.(defprotocol WereCreature "Awesom Were" (full-moon-behavior [x] "Full-moon behavior")) (defrecord WereWolf [name title] WereCreature (full-moon-behavior [x] (str name " will kill everyone"))) (full-moon-behavior (WereWolf. "David" "Master")) -
deftype,reify,proxy. reify is about implementing an anonymous protocol at runtime.