Saturday, November 26, 2016

Clojure Atom, Swap!, Reset! and Concurrency

I've encourage myself to keep writing simple Clojure codes. Obviously, this encouragement should extend beyond the traditional "Hello World!" program, shouldn't they?

Recently, I've attempted to write a in-memory database program that keeps track of person record. This in-memory database is nothing but a list (in an abstract sense) and each element in the list contains a map of properties that describes a person.

So, we'll have a variable called "db-ref" bounded to an atom of Clojure vector:


(def db-ref (atom []))



We also need a function to create a person containing id, first name, last name and email :

(defn person [pid lname fname email]

  {:id pid :lname lname :fname fname :email email})

(Does this remind you of a factory pattern?)



To add a person to the database

(defn add-person [p]

  (swap! db-ref conj p))

As you can see, we're adding the parameter "p" (for person) into db-ref using "swap!"



We also need a way to remove a person by their "id". All we have to do is use the same pattern as "add-person", but as oppose to conjoining, we use the remove function (how difficult could this be?):

(defn find-id-p [id element]
  (let [k (:id element)]
    (if (= id k)

      element)))

(defn delete-person [id]

  (swap! db-ref (into [] (remove (partial find-id-p id) @db-ref))))



Surely, our test code below should quickly tell us how awesome we are after the first attempt...

(deftest delete-person-test
  (testing
  (is (= 0 (count-db)))
  (add-person (person "100" "fred" "flintstone" "fred@flintstone"))
  (add-person (person "101" "barney" "rubble" "barney@rubble"))
  (is (= 2 (count-db)))
  (delete-person "100")
  (is (= 1 (count-db)))
  (delete-person "101")
  (is (= 0 (count-db)))))

Running "lein test":

ERROR in (delete-person-test) (APersistentVector.java:292)
Uncaught exception, not in assertion.
expected: nil
  actual: java.lang.IllegalArgumentException: Key must be integer

 at clojure.lang.APersistentVector.invoke (APersistentVector.java:292)
  ...
  ...
  ...




Oopsie! A big fat error telling us something is terribly wrong. Maybe we could sidestep the problem by using reset! function.


(defn delete-person [id]
  (reset! db-ref (into [] (remove (partial find-id-p id) @db-ref))))


Re-running the test should tell us we could totally sidestep the issue.


0 failures, 0 errors.



But before we conclude, Clojure documentation says,

(reset! atom newval)
Sets the value of atom to newval without regard for the
current value. Returns newval.

Our in-memory database may not work correctly in concurrent environments.

Alright, let's try to revisit the problem. Afterall, we're awesome aren't we?

The swap! function documentation says:

(swap! atom f) (swap! atom f x) (swap! atom f x y) 
    (swap! atom f x y & args)
Atomically swaps the value of atom to be:
(apply f current-value-of-atom args). Note that f may be called
multiple times, and thus should be free of side effects.  Returns
the value that was swapped in.

It looks like we shouldn't dereference "db-ref" when calling on swap.



Below is another take to delete a person using the swap! function:


(defn remove-person [id db-ref]
  (remove #(find-id-p id %) db-ref))

(defn delete-person [id]
  (swap! db-ref (partial remove-person id)))


We added an extra helper function to call on the actual remove function. 

Running the test displays:


0 failures, 0 errors.




No comments:

Post a Comment