Skip to content

Instantly share code, notes, and snippets.

@beatngu13
Last active June 12, 2018 10:59
Show Gist options
  • Save beatngu13/44ac58d9b38125528352d3bf835208a5 to your computer and use it in GitHub Desktop.
Save beatngu13/44ac58d9b38125528352d3bf835208a5 to your computer and use it in GitHub Desktop.
Introduction to Clojure's reference types
;;;;;;;;;;
;;;; 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