Clojure 使用了軟體事務存儲 (Software Transactional Memory,之後簡稱 STM) 模型,來處理併發與狀態管理。STM 類似於資料庫,只是它存在於記憶體之中,僅能保證 ACID 中的三種:不可分割性 (Atomicity)、一致性 (Consistency) 與隔離性 (isolation),並不保證持久性 (Durability)。
在 Clojure 中,一旦進入改變狀態的事務交易 (Transaction) 環境裡,如果其中的變化有一個不成功,則會退出視爲失敗。而其它存取狀態的執行單元並不知道交易的狀態,只會看到交易前的情形。
而 Ref 參考類型就是 Clojure 根據 STM 的實作,透過 ref
函式創建內含有狀態的 Ref 類型:
(def account (ref 0))
;; => #'user/account
要取得參考類型的值,都是使用 deref
函式或小老鼠符號 @
:
(deref account)
;; => 0
(+ 5 @account)
;; => 5
你可以使用 ref-set
更新 Ref 狀態:
(ref-set account 500)
;; => IllegalStateException No transaction running
以上範例出現的例外告訴我們,更新 Ref 狀態必須在交易 (Transaction) 中進行。建立可以安心運作的交易環境使用 dosync
:
(dosync
(ref-set account 500))
;; => 500
@account
;; => 500
現在 Ref 已更新狀態。除了 ref-set
之外,還提供以函式方式更新狀態的方法:
(dosync
(alter account + 500))
;; => 1000
@account
;; => 1000
alter
的第一個參數是 Ref 類型,之後則是函式與準備帶給函式的參數,函式將會以下列的方式呼叫:
(apply fun value-in-ref args)
dosync
創造的交易環境確保在其中進行更新的多個 Ref,在交易完成之後將會同步更新。如果在交易之中,其中一個 Ref 的狀態已經被外部改變,整個交易便會重啓,利用新的狀態再進行改變。
(def debit (ref 100000))
(def account (ref 1000))
(dosync
(alter debit - 1500)
(alter account + 1500))
;; => 2500
@debit
;; => 98500
@account
;; => 2500
如果不想因爲交易內容改變而重啓,而且交易中的執行內容不會因爲順序改變而不同,則可以考慮 commute
函式:
(dosync
(commute debit + 500)
(commute account + 500))
;; => 3000
@debit
;; => 99000
@account
;; => 3000
將 Ref 添加驗證器 (Validator) 函式會確保在更新狀態的時候,符合驗證器函式的內容,若不符合則會返回之前的狀態:
(defn validate-account
[state]
(not (neg? state)))
(def bank-account (ref 1000 :validator validate-account))
(dosync
(alter bank-account - 1500))
;; => IllegalStateException Invalid reference state
@bank-account
;; => 1000
原子類型 (Atom) 與 Ref 類型一樣都是屬於同步式:更新原子類型時必須等候先前的狀態更新完成,但是原子類型則不是協作式的,亦即無法同時更新兩個以上的原子類型,每個原子類型的更新都是與其他原子隔離的。
使用 atom
函式創造內含有狀態的原子類型,取得原子類型的值,也是使用 deref
函式或小老鼠符號 @
:
(def x (atom 10))
;; => #'user/x
@x
;; => 10
你可以使用 reset!
更新原子中的狀態:
(reset! x 20)
;; => 20
@x
;; => 20
也可以透過類似 alter
函式的 swap!
,以更新函式來更新原子類型中的狀態:
(def catherine (atom {:name "Catherine" :age 18 :graduated? false}))
;; => #'user/catherine
(swap! catherine update-in [:age] + 2)
;; => {:name "Catherine", :age 20, :graduated? false}
(swap! catherine update-in [:graduate?] not)
;; => {:name "Catherine", :age 20, :graduated? true}
@catherine
;; => {:name "Catherine", :age 20, :graduated? true}
以上範例 swap!
以 update-in
函式更新原子類型中的映射,它會以下面的呼叫方式更新映射:
(update-in @catherine [:age] + 2)
Clojure 提供了一個更新原子類型的函式,可以在更新之前先檢查狀態是否和預期的相等,若相等則更新至新值,否則不做改變:
(def x (atom 10))
(compare-and-set! x 20 30)
;; => false
@x
;; => 10
(compare-and-set! x 10 20)
;; => true
@x
;; => 20
原子類型也可以像 Ref 類型一樣添加驗證器,驗證欲改變的狀態是否符合驗證規則:
(def x (atom 100 :validator pos?))
(swap! x + 500)
;; => 600
(swap! x - 700)
;; => IllegalStateException Invalid reference state
除了驗證器之外,還可以對四種參考類型添加觀察者函式 (Watch function),一旦更新了參考類型的狀態,觀察者函式便會被呼叫。使用 add-watch
函式添加觀察者:
(add-watch x :echo
(fn [key ref old new]
(println "Key:" key)
(println "Reference:" ref)
(println "Old:" old)
(println "New:" new)))
;; => #atom[600 0x1bce3aea]
add-watch
第一個參數是準備觀察的參考類型,第二個參數則是代表新的觀察者的關鍵字標識,你可以使用這個關鍵字來參照到新的觀察者函式。
最後一個參數是欲添加的觀察者函式,觀察者函式必須接受四個參數:觀察者的關鍵字標識、觀察的參考類型、舊狀態、新狀態。
觀察者函式加上去之後,狀態一旦改變,觀察者函式便會被呼叫:
(reset! x 300)
;; => Key: :echo
;; => Reference: #atom[300 0x1bce3aea]
;; => Old: 600
;; => New: 300
;; => 300
觀察完畢後,透過觀察者的關鍵字標識與函式 remove-watch
,可以把觀察者函式從參考類型中移除:
(remove-watch x :echo)
;; => #atom[300 0x1bce3aea]
(reset! x 500)
;; => 500
從以上範例可以看到,觀察者函式移除之後,更新狀態就不會出現觀察者函式的訊息了。
(未完待續)