2021-01-03 9:30am 修正: alter-var-root のことをマクロって書いてたけど間違いで、関数なので修正しました
alter-var-root
Clojureスタイルガイドを読んでたら
https://totakke.github.io/clojure-style-guide/#alter-var
varの値を変更するには、 def の代わりに alter-var-root を使う。
って書いてあって「この alter-var-root ってちょこちょこ見かけるけど分かってないんだよなぁ」と思ったので調べた。
公式ドキュメント
いや、ちょっと前に公式ドキュメントを調べたことはあったんだけど
- alter-var-root - clojure.core | ClojureDocs - Community-Powered Clojure Documentation and Examples
- Clojure - Vars and the Global Environment
"root binding" とか、よく分からんからスルーしてた。今回こそはちゃんと調べとこっかなという気まぐれ
そもそも Var
そもそも Symbol と Var と値の関係性からよく分かってない
ので、こちらを読んだ
def と Symbol と Var の話 - (-> % read write unlearn)
この記事の中で紹介されてる Togetter の会話も読んだ
Symbol -> Var -> 値
みたいになってるってことね。なるほど。ちなみに、そんな作りになってる理由までは踏み込まないことにした。それを理解するにはもっと知識が必要そう。だから、そういうことになってるんだなぁってくらいの理解
で、これで root binding とかってどういうことなんだろう?と思ったのでソースコードを見てみることにした
雰囲気でコードを見てみる
たぶんこれが Var だな。REPL でちょこちょこブレークポイントを置いて確認してみた
clojure/Var.java at master · clojure/clojure · GitHub
それと、Namespace が色々やってそう
clojure/Namespace.java at master · clojure/clojure · GitHub
def のソースコードはどこにあるか分からなかったので、実際に def がどういう流れで登録してるのかは分かんなかったんだけど、例えば Namespace の intern メソッドはこういう感じになってて
transient final AtomicReference<IPersistentMap> mappings = new AtomicReference<IPersistentMap>(); public Var intern(Symbol sym){ ... IPersistentMap map = getMappings(); Object o; Var v = null; while((o = map.valAt(sym)) == null) { if(v == null) v = new Var(this, sym); IPersistentMap newMap = map.assoc(sym, v); mappings.compareAndSet(map, newMap); map = getMappings(); } if(o instanceof Var && ((Var) o).ns == this) return (Var) o; ... }
mappings の中に Symbol に対応している Var が登録されていなかったら生成してる。見た感じ、マルチスレッドでの扱いを前提にしてそう。
Var 自体はというと、root というオブジェクトを持っていて、これが値みたい
volatile Object root;
コンストラクターを見てみると、特に root オブジェクトを指定してない場合は Unbound になることが分かる
Var(Namespace ns, Symbol sym){ this.ns = ns; this.sym = sym; this.threadBound = new AtomicBoolean(false); this.root = new Unbound(this); setMeta(PersistentHashMap.EMPTY); }
Var にも intern というメソッドがある。こっちは static
public static Var intern(Namespace ns, Symbol sym, Object root, boolean replaceRoot){ Var dvout = ns.intern(sym); if(!dvout.hasRoot() || replaceRoot) dvout.bindRoot(root); return dvout; }
bindRoot で root にオブジェクトを bind してる。def のときは bindRoot が呼ばれてるように見えた
synchronized public void bindRoot(Object root){ validate(getValidator(), root); Object oldroot = this.root; this.root = root; ++rev; alterMeta(dissoc, RT.list(macroKey)); notifyWatches(oldroot,this.root); }
synchronized だね
ところで、alter-var-root の定義を見てみてると
(defn alter-var-root "Atomically alters the root binding of var v by applying f to its current value plus any args" {:added "1.0" :static true} [^clojure.lang.Var v f & args] (.alterRoot v f args))
Var の alterRoot メソッドを呼び出してる
synchronized public Object alterRoot(IFn fn, ISeq args) { Object newRoot = fn.applyTo(RT.cons(root, args)); validate(getValidator(), newRoot); Object oldroot = root; this.root = newRoot; ++rev; notifyWatches(oldroot,newRoot); return newRoot; }
bindRoot と比べると、違うのは、関数から値を取得しているところ以外では、alterMeta の有無だけ。alterRoot では呼び出してない
だから、alter-var-root の場合は Var のメタ情報は変更されないみたいね
それはそうと get メソッドを眺めてたら、root と呼ばれる意味が分かった気がする
final public Object get(){ if(!threadBound.get()) return root; return deref(); } final public Object deref(){ TBox b = getThreadBinding(); if(b != null) return b.val; return root; }
設定によってはスレッドローカルに違う値をバインドして使えるみたい。なので、全スレッドで使われる値のことを root と呼んでるっぽいな(僕の想像)
整理
ざっと読んだところで整理
- Namespace には全スレッドから参照可能な mappings を持っていて、そこに
- Symbol -> Var が登録されている
- Var には root オブジェクトが登録できて
- スレッドローカルなバインディングが登録されてたらその値を
- 登録されていない場合は root オブジェクトを返す
- def と alter-var-root がそれぞれ root オブジェクトの変更に使ってるメソッドにはあまり違いはなさそう
ここまでで、alter-var-root が何をやってるのかはだいたい分かったのでOK。次の疑問はこれ
def と alter-var-root の違いって何だろう?
alter-var-rootは
— Mitsuyuki Shiiba (@bufferings) January 2, 2022
* symbolのmappingsが未定義の場合にエラーになる
* 別のネームスペースの値も扱うことができる
* 現在の値を元にAtomicに変更を加えることができる
だし、defを2回以上呼び出すのは良くないhttps://t.co/lb4PHq8rHh
スッキリした。基本的には def で定義されたものを自分が変更することはないだろうけど、ライブラリとか読んでて見かけたときも問題なく読めそうになった。よかった。
さて、Clojureスタイルガイドの続き読もう。あ、いや、そろそろ寝よう。