Clojure の alter-var-root って何だろう?

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 ってちょこちょこ見かけるけど分かってないんだよなぁ」と思ったので調べた。

公式ドキュメント

いや、ちょっと前に公式ドキュメントを調べたことはあったんだけど

"root binding" とか、よく分からんからスルーしてた。今回こそはちゃんと調べとこっかなという気まぐれ

そもそも Var

そもそも Symbol と Var と値の関係性からよく分かってない

ので、こちらを読んだ

def と Symbol と Var の話 - (-> % read write unlearn)

この記事の中で紹介されてる Togetter の会話も読んだ

Clojure の Var について - 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 の違いって何だろう?

スッキリした。基本的には def で定義されたものを自分が変更することはないだろうけど、ライブラリとか読んでて見かけたときも問題なく読めそうになった。よかった。

さて、Clojureスタイルガイドの続き読もう。あ、いや、そろそろ寝よう。