bindingとvarについて

bindingってなんだろう? プログラミングClojureを読んでいまいちピンと来なかったので考えてみました。

bindingは新しい変数を作らない

;; program-1
(let [x 10] x)     ;=> 10
(binding [x 10] x) ;=> java.lang.Exception: Unable to resolve var: x in this context...

let はレキシカル変数を束縛する。新しい変数 x が作られたことになる。let式内では新たに作られたレキシカル変数 x の値を参照する。
binding はレキシカル変数を束縛しない。新しい変数 x は作られない。binding式内で x を参照しても x という変数は存在しないのでエラーになる。
binding式はlet式とよく似てるので 10 に束縛された x が存在しているように見えるかもしれない(自分はそう錯覚した)けれど新らたな変数は作られないらしい。let式とは全く違う。

動的なvarのルート束縛

var ってなんだ? ルート束縛ってなんだ? わかりにくいですね。
要するに def で名前束縛をすると「動的なvar」というモノが新たに作られ、これに値を束縛することを「ルート束縛」と呼ぶらしい。

;; program-2
(def x 100)
(let [x 10] x)     ;=> 10
(binding [x 10] x) ;=> 10
x                  ;=> 100

def により、x をルート束縛しました。この x は動的なvarとよばれるモノに該当します。
let式の結果は program-1 から変化ありません。100にルート束縛された x は、レキシカル変数 x によって名前が隠されてしまうため let式内の x は 10 と評価されます。
一方binding式は状況が変りました。エラーにならず評価値 10 を返しています。
binding式は新たな変数を作りません。では式の中の x は何者かというとこれは def でルート束縛された x だと考えるしかないでしょう。つまり binding はルート束縛されている x の値を 10 に書き換え(あるいは再束縛?)していることになります。ただしその後の x の評価値が 100 になっていることから、binding による再束縛の有効範囲は binding式内に限られることが分ります。この仕様は次のように書くとよりハッキリします。

;; program-3
(def x 100)  ;=> user/x が 100 に束縛(ルート束縛)される。

(let [x 10] [x user/x (identical? x user/x)])
 ;=> [10 100 false]

(binding [x 10] [x user/x (identical? x user/x)])
 ;=> [10 10 true]

x ;=> 100

clojureでは ns式により名前空間を作ることができます。名前空間の宣言を省略するとデフォルト名前空間として「user」が適用されます。そして変数は「名前空間/変数名」のように名前修飾を付けて参照することが出来ます。
また def で束縛された名前は、束縛がおこなわれた名前空間に属します。「ルート束縛」という言葉から「全ての名前空間の上位に『ルート名前空間』が存在しそこに属する」と勘違いしそうです(自分はそう誤解した)がそうではなく、あくまでも def 式が書かれたその名前空間に属します。ルート名前空間などという物はありません。
したがって program-3 の一行目は「user/x を 100 に束縛している」ことになります。
let式の中の無修飾の x はレキシカル変数であり、名前空間修飾を付けた user/x はルート束縛された x ということになります。評価値も期待通りです。オブジェクトの同一性を調べる identical? の評価値も両者が異なる変数であること(false)を示しています。
一方binding式は、x も user/x もどちらも評価値が 10 です。identical? も同一であること(true)を示しています。ルート束縛された user/x の値が書き換えられていることが明白です。

set!による書き換え

set! は変数の値を書き換えるフォームです。ただし書換えができるのは binding式の中に限られるようです。

;; program-4

;; ルート束縛
(def x 100) 
(def y 200)

;; バインドした x は set! で書き換え可能
(binding [x 10] (do (print x "") (set! x 20) (print x "")))
 ;=> 10 20 nil

;; user/x も結局同じ変数なので書き換え可能
(binding [x 10]
  (do (print user/x "") (set! user/x 20) (print user/x "")))
 ;=> 10 20 nil

;; binding式の中でもバインドしていない変数(y)は書き換えできない
(binding [x 10] (do (print y "") (set! y 20) (print y "")))
;=> java.lang.IllegalStateException: Can't change/establish root binding of: y with set

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 以下すべて binding式ではないので set! はエラーになる。

(let [x 10] (do (print x " ") (set! x 20) (print x " ")))
;=> java.lang.IllegalArgumentException: Invalid assignment target ...

(let [x 10]
  (do (print user/x " ") (set! user/x 20) (print user/x " ")))
;=> java.lang.IllegalStateException: Can't change/establish root binding of: x with set ...

(set! x 20)
;=> java.lang.IllegalStateException: Can't change/establish root binding of: x with set ...

エラーメッセージを見返してみる

;; program-1 より
(binding [x 10] x) ;=> java.lang.Exception: Unable to resolve var: x in this context...

ルート束縛のない変数をbinding しようとすると「varが解決できない」と怒られます。ルート束縛で作られるのは「動的なvar」でした。varが定義されていないので解決できなかったということですね。

;; program-4 より
(def x 100)

(set! x 20)
;=> java.lang.IllegalStateException: Can't change/establish root binding of: x with set ...

(let [x 10] (do (print x " ") (set! x 20) (print x " ")))
;=> java.lang.IllegalArgumentException: Invalid assignment target ...

前者、establish は「根付く」「設置する」という意味。「ルート束縛された x は、値を変更できない」という意味でしょうか。
後者は ArgumentException。レキシカル変数を set! の引数にするとこうなるようです。

bindingによるスレッドローカル変数

binding式でバインドした変数はスレッドローカルになります。これについては各所で言及されているので詳細はいりませんね。

動的varと静的var

Clojureでは単に「var」と言うと「動的なvar」を指すそうです。動的なvarとはつまり def によりルート束縛されたvarです。何故「動的」と呼ばれるのでしょうか?
Clojureでは本来変数はimmutableのはずですが、このbindingとset!フォームに限っては変数がmutableになっているようです。その対象に出来るのはルート束縛されたvarだけです。レキシカル変数は対象になりません。
そういった背景を考えると、ルート束縛変数が動的というのは分かるきがします。またレキシカル変数は「静的var」なのかもしれません。

補足

いうまでもありませんが、clojurelisp-1ですので def による束縛について言えることはすべて、defn による関数束縛についても当て嵌ります。