ClojureのVar

前置き

Clojureでは、変更可能(mutable)なオブジェクトは、VarとRefとAgentとAtomの4つだけです。本記事では、その中の1つ、Varオブジェクトについて整理します。

Varの生成

Varを作るにはdef特殊フォームを使います。defは、生成したVarオブジェクトを返します。

user=> (def foo "Hello")
#'user/foo

defの結果として表示された「#'user/foo」は、「userという名前空間の、fooという名前のVarオブジェクト」という意味です。

上記コードでは、fooという名前のVarを作り、値として"Hello"という文字列をバインドしています。図示すると下記のような関係になります。

Var

点線は名付け対象を示しています。実線矢印はバインド先を指します。このように、Varオブジェクトは、何らかの値(Stringオブジェクトとか関数オブジェクトとか)を指すポインタのようなものと考えて良いでしょう。def以外でも、defを使って実装されたマクロ(例えばdefn)を呼べば、Varオブジェクトが生成されます。

Clojureでは、Symbolを評価すると、名付け対象のVarがバインドした値が返ります。

user=> foo
"Hello"
user=> (class foo)
java.lang.String

バインドした値ではなく、Varオブジェクトそのものを得るには、var特殊フォームか、Varクオート・リーダマクロを使います。

user=> (var foo)
#'user/foo
user=> #'foo       ;;Varクオート。
#'user/foo
user=> (class #'foo)
clojure.lang.Var

同様に、Symbolオブジェクトそのものを得るには、クオート・リーダマクロを使います。

user=> 'foo        ;;クオート。
foo
user=> (class 'foo)
clojure.lang.Symbol

defするときに、値を省略することも可能です。その場合、値がバインドされてない(unboundな)Varオブジェクトが生成されます。

user=> (def bar)
#'user/bar
user=> bar
#<Unbound Unbound: #'user/bar>

名前空間との関係

defによりVarオブジェクトを生成すると、名前(つまりSymbol)とVarオブジェクトとの対応が名前空間に登録されます。これを「インターン(intern)する」と言います。また、名前に対応するVarオブジェクトを得ることを「解決(resolve)する」と言います。

ns-interns関数を使うと、インターンされた名前とVarオブジェクトとの対応を見ることができます。

user=> (pprint (ns-interns 'user))
{clojuredocs
 ...中略...
 foo #<Var@337c4232: "Hello">,
 ...中略...}
nil

名前を解決するには、resolve関数を使います。解決した結果が、他所の名前空間のVarになる可能性もあります。

user=> (resolve 'foo)
#'user/foo
user=> (resolve '*out*)
#'clojure.core/*out*

バインドの変更と、Varの再定義

Varオブジェクトのバインドを変更するには、alter-var-root関数を使います。

user=> (def foo "Hello")
#'user/foo
user=> foo
"Hello"
user=> (alter-var-root #'foo #(str % "!"))
"Hello!"
user=> foo
"Hello!"

alter-var-rootの最初の引数には、変更したいVarオブジェクトを指定します(要Varクオート)。第2引数は、新しい値を計算するための関数です。上記の例では、無名関数を使いました。alter-var-rootを呼ぶと、この無名関数がコールバックされ、その引数にfooの現在値が指定されます。無名関数が返す値が、fooの新しい値になります。

alter-var-rootによるバインドの変更は、アトミックに行われます。つまり、複数のスレッドが同時にalter-var-rootを呼んでも、安全にバインドが更新されます。またalter-var-rootは、任意の名前空間のVarオブジェクトに対して使うことができます。

user=> clojure.core/*print-dup*
false
user=> (alter-var-root #'clojure.core/*print-dup* (fn [_] true))
"true"             ;;引用符で囲まれるのは、*print-dup*を変更した副作用。
user=> clojure.core/*print-dup*
"true"

バインドを変更するもう一つの手段は、同じ名前を使って、再度defすることです。defは、もし指定された名前がインターン済みなら、そのVarオブジェクトを再利用しつつ、バインド先を変更します。

user=> (def foo "Hello")
#'user/foo
user=> (.hashCode #'foo)
880453696               ;;Varオブジェクトのハッシュ値。
user=> (def foo "Hello!")
#'user/foo
user=> (.hashCode #'foo)
880453696               ;;再defしても、Varオブジェクトは同じ物。
user=> foo
"Hello!"

alter-var-rootと再defの明確な使い分けルールは無いようですが、以下をヒントにすれば良いでしょう。

  • alter-var-rootによるバインドの変更はアトミックだが、defはアトミックではない
  • alter-var-rootは任意の名前空間に対して使えるが、defは現在の名前空間にしか使えない
  • alter-var-rootは、既にdef済みのVarオブジェクトにしか使えない

私見ですが、生成時はunboundにしておき、あとから値をセットしたいようなケースでは、再defを使う気がします。一方、何らかの副作用(デバッグ用にログレベルを変えるとか)を起こすために、フラグ的なVarを変更したいようなケースでは、alter-var-rootを使うでしょう

ルートバインドと、スレッドローカルバインド

実は、Varオブジェクトと値とのバインドには、2種類あります。これまで述べてきたバインドは、ルートバインドの話しでした。alter-var-rootの"root"は、ルートバインドの「ルート」です。またdefの第2引数には、ルートバインドしたい値を指定します。デフォルトでは、このルートバインドのみが有効です。

Varオブジェクトのメタデータとして ^{:dynamic true}を付与すると、ルートバインドに加えて、スレッドローカルバインドも有効になります。スレッドローカルバインドの特徴は以下の通りです。

  • ルートバインドされた値より優先して参照される
  • 値をバインドしたスレッドのみが参照できる(他のスレッドからは見えない)
  • バインドは一時的(値をバインドしたブロックから抜けるとバインドは解ける)
  • バインドのブロックはネストできる

スレッドローカルバインドするには、bindingマクロを使います。簡単なコードで確認してみましょう。

user=> (def ^:dynamic foo "hello")              ;;①
#'user/foo
user=> (defn prn-foo [] (prn foo))              ;;fooを表示する関数。
#'user/prn-foo
user=> (do
     (prn-foo)
     (binding [foo "hi"] (do                    ;;②
                           (prn-foo)
                           (binding [foo "hey"] ;;③
                             (prn-foo))
                           (prn-foo)))
     (prn-foo))
"hello"
"hi"
"hey"
"hi"
"hello"
nil

①でルートバインドし、②と③でスレッドローカルバインドしています。ブロック②の中にブロック③がネストした形になっている点に注意して下さい。そのため、ブロック③の中ではfooは"hey"ですが、ブロック③の前後(ブロック②の中)では"hi"になっています。そしてブロック②から抜けると、ルートバインドの"hello"に戻ります。

まるで、fooという1つのVarオブジェクトが、複数のスコープ(①と②と③)を持っているように見えます。しかも、スレッドローカルバインドによるスコープは、実行時に作られます(静的には決まらない)。このことから、こういったスコープの仕様はダイナミックスコープと呼ばれます。一方、関数の引数やlet特殊フォームにおけるバインドのスコープは静的に決まるので、スタティックスコープとかレキシカルスコープと呼ばれます。

set!特殊フォームを使うと、bindingによるスレッドローカルバインドを変更することができます。ただし、この変更は、当該bindingブロック内のみで有効です。上記のコードに1行だけ追加して、set!の挙動を確認してみましょう。

user=> (do
     (prn-foo)
     (binding [foo "hi"] (do                    ;;②
                           (prn-foo)
                           (set! foo "hoo")     ;;☆
                           (binding [foo "hey"] ;;③
                             (prn-foo))
                           (prn-foo)))
     (prn-foo))
"hello"
"hi"
"hey"
"hoo"
"hello"
nil

☆の行を追加しました。fooの値を"hoo"に変更しています。これは、☆以降、かつ②の中、かつ③の外で有効です。

bindingの外でset!を使うとエラーになります。また、スレッドローカルバインドが有効になってない(つまり、メタデータの:dynamicがfalseの)Varに対してbindingを使ってもエラーになります。さらに、Lispには、ダイナミックスコープの変数に *foo* のような名前を使う習慣があるので、(def *foo* "hello")などとすると警告が出ます。

結局のところ、Varのベストプラクティスは?

私見ですが、Varの「変更可能(mutable)」という特性を利用する場面は、それほど多くないと思います。実際のところ、大多数のVarオブジェクトは「定数的」なのではないでしょうか。つまり…

  • ルートバインドのみを使う
  • 関数型プログラミングの作法にならって、一旦、値をバインドしたら変更しない
  • defやdefnにより、値(や関数)に名前を付けるのが目的
  • alter-var-rootは使わない

Varは至る所にウヨウヨ存在しています。関数型スタイルのコードでも、defやdefnの使用は避けられません。Varをmutableに使ってしまうと、関数型スタイルの(pureな)コードと命令型スタイルの(impureな)コードの区別がつかなくなってしまいます。単にmutableなデータが欲しいだけなら、Varよりも、RefやAgentやAtomを使った方が良いでしょう。この3つは決定的にimpureですから。

ただ、スレッドローカルバインド機能はVar以外では代替できません。よって、ダイナミックスコープが必要な場面では、Varを使うことになります。この場合でも、:dynamicというキーワードによりimpureが明示されるので、pureな世界と区別することは可能です。ちなみに、バージョン1.3より前のClojureではVarはデフォルトでダイナミックスコープを持っていましたが、この仕様はバージョン1.3で改められ、ダイナミックスコープOFFがデフォルトになりました。

Last modified:2013/08/25 23:36:01
Keyword(s):
References:[メモ ~ Clojureチュートリアル] [FP: 関数型プログラミング]
This page is frozen.