Clojure の def はどう動いてるの?

Where is def?

昨日の記事の中で

bufferings.hatenablog.com

def のソースコードはどこにあるか分からなかったので、実際に def がどういう流れで登録してるのかは分かんなかったんだけど

って書いたけど、Clojure は JAR ファイルで配布されてるんだから、その中のどっかにあるはずだよなぁと思って、確認したら見つけた。ヽ(=´▽`=)ノ

clojure/Compiler.java at master · clojure/clojure · GitHub

というか、昨日の時点で REPL のコールスタックを確認すればすぐに気付けたことだった。寝て起きるの大切。

How does def work?

じゃ、def の流れを追いかけてみよう。Cursive の REPL でデバッグ実行しながら def してみる。例によって雰囲気です!

(def abcde "foobar")

じっこうー!

eval

まず最初にここに入ってくる↓

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Compiler.java#L7135

public static Object eval(Object form) {
    return eval(form, true);
}

public static Object eval(Object form, boolean freshLoader) {

上の方が呼ばれて、すぐ下に入ってきて、引数はこんな感じ↓

f:id:bufferings:20220103124531p:plain:w400

昨日ちょっと見た Var を使って、スレッドローカルに行番号などの情報を保持しつつ、今回は関係ないけど macroexpand が呼び出されて、今回の def の場合は、ここに入る↓

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Compiler.java#L7185-L7186

Expr expr = analyze(C.EVAL, form);
return expr.eval();

まず Expr オブジェクトを取得してから、そのオブジェクトの eval() を呼び出してる

analyze

analyze で Expr オブジェクトを取得してる。まずはここに入ってくる↓

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Compiler.java#L6748

public static Expr analyze(C context, Object form) {
    return analyze(context, form, null);
}

private static Expr analyze(C context, Object form, String name) {

context には "EVAL" が入ってる。これも上の方に入ってきてすぐ下のメソッドが呼び出されるので name は null

form に合わせて分岐に入るんだけど、さっき画像で見た通り3つの要素を持った PersistentList なので、入るのはこの分岐↓

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Compiler.java#L6793

else if(form instanceof ISeq)
        return analyzeSeq(context, (ISeq) form, name);

analyzeSeq

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Compiler.java#L7085

form の最初の要素をチェックしてここに来る↓

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Compiler.java#L7110-L7111

else if((p = (IParser) specials.valAt(op)) != null)
    return p.parse(context, form);

この specials はここで定義されていて↓

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Compiler.java#L105

内容はこうなってるので↓

static final public IPersistentMap specials = PersistentHashMap.create(
        DEF, new DefExpr.Parser(),

DefExpr の Parser の parse() が呼び出される。近づいてきたー!

parse

ということで parse が呼び出されて↓

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Compiler.java#L530

static class Parser implements IParser{
    public Expr parse(C context, Object form) {

↓ここで form の2番目の要素から Symbol を取得して、その Symbol を使って Var を lookup してる

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Compiler.java#L544

Symbol sym = (Symbol) RT.second(form);
Var v = lookupVar(sym, true);

↓lookupVar の中ではここにたどり着いて、現在のネームスペースのマッピングに Symbol がなければ intern して新しい Var をマッピングに登録。あればそれをそのまま返している

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Compiler.java#L7485

Object o = currentNS().getMapping(sym);
if(o == null)
    {
    //introduce a new var in the current ns
    if(internNew)
        var = currentNS().intern(Symbol.intern(sym.name));
        }
else if(o instanceof Var)
    {
    var = (Var) o;
    }

で、最終的には DefExpr のコンストラクターが呼ばれて、生成されたインスタンスが返される

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Compiler.java#L424

コンストラクターの引数はこう↓

f:id:bufferings:20220103124713p:plain:w500

Var は初回だと Unbound。二回目以降だと元々マッピングされてた Var なので前回の値が root にバインドされてる状態

今回の場合は def に値を渡してるので、それが init として渡されてて、initProvided が true になってる

これで DefExpr のインスタンスが取得できた!ので、次にそのインスタンスに対して eval ()を呼び出す

eval()

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Compiler.java#L449

eval はとてもシンプルで、初期値を指定している場合には bindRoot を呼び出してる

var.bindRoot(init.eval());

おわりー!

すっきり

昨日のチェックをもう一歩踏み込んで確認できた。すっきり。これで、何か分かんないことがあっても多少はソースコードを調べられそう!