Last active
June 12, 2018 10:59
-
-
Save beatngu13/44ac58d9b38125528352d3bf835208a5 to your computer and use it in GitHub Desktop.
Introduction to Clojure's reference types
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;;;;;;;;;; | |
;;;; Introduction to Clojures reference types based on | |
;;;; http://clojure-doc.org/articles/language/concurrency_and_parallelism.html#clojure-reference-types | |
;;;;;;;;;; | |
;; Clojure provides a powerful set of reference types, each of them with its own | |
;; concurrency semantics for different kinds of operations: | |
;; | |
;; | Coordinated | Uncoordinated | |
;; ------|-------------|-------------- | |
;; Sync | Refs | Atoms | |
;; Async | N/A | Agents | |
;; | |
;; Coordinated means operations relate on each other, e.g. transfering money | |
;; from one bank account to another. Synchronous means the calling thread will | |
;; wait, block, or sleep until it receives the desired result. | |
;; Uncoordinated/asynchronous stands for the respective opposite. | |
;;;;;;;;;; | |
;;; Vars | |
;;;;;;;;;; | |
;; Vars are clojures standard reference type which can be defined via def. | |
(def static-var 4711) | |
;; Functions stored in vars are refs as well since (defn ...) is a shorthand for | |
;; (def (fn ...)). Vars are dynamically scoped and have root bindings that are | |
;; initially visible to all threads. | |
(.start (Thread. (println static-var))) | |
;;=> Prints "4711". | |
(println static-var) | |
;;=> Prints "4711" as well. | |
;; To temporarily change a var's value, they need to be make dynamic by adding | |
;; metadata. Per convention dynamic vars are named with leading and trailing | |
;; asterisks. Afterwards they can have thread-local bindings. | |
(def ^:dynamic *dynamic-var* 1337) | |
(.start (Thread. (binding [*dynamic-var* 4711] | |
(println *dynamic-var*)))) | |
;;=> 4711 | |
;;=> nil | |
(println *dynamic-var*) | |
;;=> 1337 | |
;;=> nil | |
;; If needed the root binding of a var can be altered as well. alter-var-root | |
;; takes a var (not its value) and a function that returns the new value. The | |
;; var function is used to locate the var. | |
(.start (Thread. | |
(do | |
(alter-var-root (var static-var) (constantly 42)) | |
(println static-var)))) | |
;;=> 42 | |
;;=> nil | |
(println static-var) | |
;;=> 42 | |
;;=> nil | |
;;;;;;;;;; | |
;;; Atoms | |
;;;;;;;;;; | |
;; Atoms are references that change atomically, visible for all | |
;; threads. Basically, they are references from java.util.concurrent with a | |
;; functional twist, since they need a function to be "mutated". These functions | |
;; must be pure (referentially transparent and without side effects). This | |
;; enables Clojure to retry the operation safely. | |
(def active-connections (atom [])) | |
;; In order to read atoms, they must be dereferenced. Either with the deref | |
;; function or with the @ Reader macro. | |
(deref active-connections) | |
;;=> [] | |
@active-connections | |
;;=> [] | |
active-connections | |
;;=> #object[clojure.lang.Atom 0x6461ce01 {:status :ready, :val []}] | |
;; To mutate atoms, swap! must be used. The function takes an atom, a function | |
;; and optionally additional parameters, in case the supplied function needs | |
;; them. The resulting value of this function will be the new value of the atom. | |
(swap! active-connections conj :some-key) | |
;;=> [:some-key] | |
@active-connections | |
;;=> [:some-key] | |
;; Though it should be avoided, reset! allows to set a new value to an atom. | |
(reset! active-connections []) | |
;;=> [] | |
@active-connections | |
;;=> [] | |
;;;;;;;;;; | |
;;; Refs | |
;;;;;;;;;; | |
;; Refs use transactions and therefore provide ACI(D). They are backed by | |
;; Clojure's implementation of software transactional memory (STM), which uses | |
;; multiversion concurrency control (MVCC). That means, it does mutation by | |
;; taking a snapshot of the ref, making the changes in isolation to the | |
;; snapshot, and apply the result. If the STM detects that another transaction | |
;; has made an update to the ref, the current transaction will be forced to | |
;; retry. | |
;; To instantiate a ref, the ref function must be used. Like atoms, they also | |
;; must dereferenced for reading. | |
(def pos-or-zero? (complement neg?)) | |
(def account1 (ref 100 :validator pos-or-zero?)) | |
(def account2 (ref 0 :validator pos-or-zero?)) | |
;; Since refs are for coordinated operations, modifications must be done with | |
;; some sort of synchronization over two or more refs. This can be achieved with | |
;; dosync, which starts a transactions that automatically retries. Within the | |
;; dosync body the function alter is used, which is similar to swap! in its | |
;; arguments. | |
(defn transfer [amount from to] | |
(dosync | |
(alter from - amount) | |
(alter to + amount))) | |
(do | |
(println "Initial balance: account1 =" @account1 "acount2 =" @account2) | |
(transfer 100 account1 account2) | |
(println "Balance after transfer: account1 =" @account1 "acount2 =" @account2)) | |
;; If mutations are done frequently and order doesn't matter, commute can be | |
;; used instead of alter. Functions handed over to commute muste be commute in | |
;; the mathematical sense. Therefore, transactions don't need to be retried. | |
(dosync | |
(commute account1 + 100) | |
(commute account2 + 200)) | |
;; As already mentioned, functions must be pure in order to enable Clojure to | |
;; retry transactions. For instance, operations on the filesystem are often not | |
;; idempotent nor can be undone. It's up to developer to respect this | |
;; limitations. Nevertheless, io! is helper function which raises an exception | |
;; if I/O access is detected. | |
(dosync (io! (println "Ooops!"))) | |
;;;;;;;;;; | |
;;; Agents | |
;;;;;;;;;; | |
;; Agents are references for asynchronous and uncoordinated operations, but | |
;; they're not covered here. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment