Skip to content

Instantly share code, notes, and snippets.

@kohyama
Last active February 9, 2023 05:35
Show Gist options
  • Save kohyama/6076544 to your computer and use it in GitHub Desktop.
Save kohyama/6076544 to your computer and use it in GitHub Desktop.
Clojure ref, atom, agent の要約

Clojure ref, atom, agent の要約

  • 作成: ref
  • 参照: deref または @
  • 変更: dosync で包んで
    • ref-set: 上書き
    • alter: 関数を適用して再代入(順序を保証)
    • commute: 関数を適用して再代入(順序保証なし)

作成

(def x (ref '(1 2 3)))

参照

(deref x) ; -> (1 2 3)
@x ; -> (1 2 3)

変更/上書き

ref の変更は dosync で包むことで, 複数の変更のアトミシティが保証されるが, 単独の変更の場合でも, dosync で包む必要がある.

(ref-set x '(2 3 4))
; -> java.lang.IllegalStateException: No transaction running (NO_SOURCE_FILE:0)
(dosync (ref-set x '(2 3 4)) ; -> (2 3 4)
@x ; -> (2 3 4)

dosync で包めば複数の変更が外からアトミックに見える.

(def y (ref (count @x)))
(dosync
  (ref-set x (cons 8 @x))
  (ref-set y (count @x)))

上記で「x は変更されたが, x の変更に y が追随していない」という状態が他のスレッドから見えないことが保証される.

変更/関数を適用して再代入

r が ref の時

(alter r fn args...)

(ref-set r (fn @r args...))

と等しい.
従って, cons ではなく conj を使うと引数順序が適合する.

(dosync
  (alter x conj 9)
  (ref-set y (count @x)))

dosync と alter は MVCC (Multiversion Concurrency Control, 多版型同時実行制御) で動いている.
トランザクションはローカルコピーに対する変更を行い その間に他のトランザクションが ref を書き換えていたら dosync は再起動される.
(dosync から例外で抜けた場合はこの限りではない)

commute は更新順序が違ってもいい場合 使い方は alter と同じ

バリデータ

ref を作るときに :validater キーワードで引数を一つ取り, bool 値を返す関数を指定しておけば, ref の更新時に値にその関数を 適用して false を返すならば例外を投げてくれる

(def x (ref () :validator (partial every? even?)))
(dosync (alter x conj 2)) ; -> (2)
(dosync (alter x conj 3))
; -> java.lang.IllegalStateException: Invalid reference state (NO_SOURCE_FILE:0)
(dosync (alter x conj 4)) ; -> (4 2)

詳細はこちらのスライド が詳しいです.

ref は dosync で包み, 複数の変更を協調させて使う. atom は他の ref や atom と協調する必要が無いときに使う.

  • 作成: atom
  • 参照: deref または @
  • 変更:
    • reset!: 上書き
    • swap!: 関数を適用して再代入

ref と同様 :validator が使える

(def x (atom () :validator (partial every? even?))) ; -> #'user/x
@x ; -> ()
(reset! x (conj @x 2)) ; -> (2)
(reset! x (conj @x 3)) ; -> java.lang.IllegalStateException: ...
(reset! x (conj @x 4)) ; -> (4 2)
(swap! x conj 5) ; -> java.lang.IllegalStateException: ...
(swap! x conj 6) ; -> (6 4 2)

関数が返った時点で値の更新が保証されなくていいなら(遅れていいなら)

  • 作成: agent
  • 参照: deref または @
  • 変更:
    • send: 関数を適用して再代入

上書き変更 (ref-set や reset! のような) は提供されていない.
:validator は ref や atom と同様に使えるが, 例外は send が呼ばれたときではなく, 後の更新操作時に RuntimeException: Agent is failed を返す.

(def x (agent () :validator (partial every? even?))) ; -> #'user/x
@x ; -> ()
(send x conj 2) ; -> #<Agent@.... (2)>
@x ; -> (2) 反映が遅れて () が返るかもしれない.
(send x conj 3) ; -> #<Agent@.... (3)>
@x ; -> (2)
(send x conj 4) ; -> java.lang.RuntimeException: Agent is failed...

エラーが起きていたかどうかは agent-errors で調べられる.

(agent-errors x) ; -> (#<IllegalStateException java.lang.Illegal...>)

clear-agent-errors で復帰する.

(clear-agent-errors x) ; -> (2)
(send x conj 4) ; -> #<Agent@.... (4 2)>
@x ; -> (4 2) <- くどいようだがこれは保証されない.

await で全ての send された変更が反映されるのを待つことができる.

(def x (agent () :validator (partial every? even?))) ; -> #'user/x
(send x conj 2) ; -> #<Agent@.... (2)>
(send x conj 4) ; -> #<Agent@.... (4 2)>
(await x) ; -> nil (これはブロックする.)
@x ; -> (4 2) これは保証される.

更新の反映を確認してブロックせずに返すには await-for を使い, ミリ秒でタイムアウトを指定する.

(send x conj 6) ; -> #<Agent@.... (6 4 2)>
(send x conj 8) ; -> #<Agent@.... (6 4 2)>
(await 100 x) ; -> タイムアウトしたら nil, そうでなければ nil 以外を返す.
@x ; -> (8 6 4 2)

await の返り値が nil 以外なら @x(8 6 4 2) であることが保証される. await の返り値が nil なら @x(4 2) かもしれないし, (6 4 2) かもしれない.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment