Overthunk

Cuddle Zombies contd...

Jul 19, 2020


Contents

Chapter 10

How does Clojure handle two threads calling swap! on an atom? By using compare-and-set semantics.

  1. It reads the current state of the atom.
  2. It then applies the update function to that state.
  3. Next, it checks whether the value it read in step 1 is identical to the atom’s current value.
  4. If it is, then swap! updates the atom to refer to the result of step 2.
  5. If it isn’t, then swap! retries, going through the process again with step 1.

swap! happens synchronously. reset! updates an atom without checking it’s current value.

Atoms are good for managing the state of independent entities. Use Refs for managing the state of more than one identity simultaneously.

Refs

Refs allow you to update the state of multiple identities using transaction semantics.

Modify a ref using alter inside a transaction. Altering a ref doesn’t make the change immediately visible outside the transaction.

It’s like the transaction has its own private area for trying out changes to the state and the rest of the world can only learn about it at the end of the transaction.

(def counter (ref 0))
(future
  (dosync
   (alter counter inc)
   (println @counter)
   (Thread/sleep 500)
   (alter counter inc)
   (println @counter)))
(Thread/sleep 250)
(println @counter)

The value of the ref within the future will be different compared to the value outside of it. refs also use a similar compare-and-set method to determine if a change should be made.

alter - Reach outside the transaction and read the ref’s current state. Compare the current state to the state the ref started with within the transaction. If the two differ, make the transaction retry. Otherwise, commit the altered ref state.

commute - Reach outside the transaction and read the ref’s current state. Run the commute function again using the current state. Commit the result.

commute doesn’t force a transaction retry. Improved performance but it’s possible for a ref to end up in an inconsistent state.

Dynamic Vars

(def ^:dynamic *notification-address* "dobby@elf.org")

;; temporarily change the value of a dynamic var
(binding [*notification-address* "test@elf.org"]
  *notification-address*)
;; => "test@elf.org"

*notification-address*
;; => "dobby@elf.org"

Dynamic vars have some interesting use cases -

(def ^:dynamic *troll-thought* nil)
(defn troll-riddle
  [your-answer]
  (let [number "man meat"]
    (when (thread-bound? #'*troll-thought*) ;; checks if the var has been bound
      (set! *troll-thought* number))
    (if (= number your-answer)
      "TROLL: You can cross the bridge!"
      "TROLL: Time to eat you, succulent human!")))

(binding [*troll-thought* nil]
  (println (troll-riddle 2))
  (println "Succulent Human: OOOH! The answer was " *troll-thought*))

Temporarily alter a var’s root with with-redefs -

(with-redefs [*out* *out*]
  (doto (Thread. #(println "with redefs allows me to show up in REPL"))
    .start
    .join))

Useful for testing where you can redefine a function that returns data from a network call, so that the function returns mock data without having to actually make a network request.

Takeaways

Whew! We’ve just gone through quite a lot! Brave Clojure goes way deeper into these concurrency primitives than Clojure from the ground up and we can really see how they are implemented and what they can be used for in real-life (not just in hypothetical zombie apocalypses).

I’m almost done with Chapter 10. There is still a little bit left (pmap and the exercises) that I will tackle tomorrow.

I also need to read more in-depth about some of the topics mentioned in the chapter -