TL;DR

  • var は統一的な更新モデルに従わず、スレッドローカルな状態を管理する。^:dynamic を付けた var は binding でダイナミックスコープを持つスレッドローカルな束縛を作れる。
  • 動的束縛は離れたところから関数の動作を変えられる(e.g. メモ化した関数に差し替える)が、関数が純粋でなくなるので、基本的には使わない。
  • シングルスレッドで動く Java のコールバック API では、動的束縛と set! を使う。

内容

var は統一的な更新モデルに従わず、スレッドローカルな状態を管理するのに使う。

var の生成

def や defn で var を作れる。

(def foo 10) ; #'user/foo

foo の束縛はすべてのスレッドで共有されるので、もちろん自分のスレッドから参照できる。

foo ; 10

foo の値を別のスレッドから調べることもできる。新しいスレッドを作り、foo の値を表示する関数を実行して確かめる。

(.start (Thread. (fn [] (println foo)))) ; nil
;; 10

これを実行すると、start() の呼び出しが nil を返し、foo の値が新しいスレッドによって表示される。

動的な var

通常の var はダイナミックスコープで束縛できないが、メタデータ :dynamic を付けると、binding を使ったダイナミックスコープでの束縛を使える。

(def ^:dynamic foo 10)

binding マクロ

ほとんどの場合、var はルート束縛を永遠に持ち続ける。しかし、binding マクロを使うと、var にスレッドローカルな束縛を作れる。

(binding (bindings) & body)

binding による束縛はダイナミックスコープを持つ。つまり、実行したスレッドが binding の本体の実行を抜けるまでの間、そのスレッドのあらゆる箇所から参照できる。他のスレッドからは、この束縛は見えない。構造は let とよく似ている。

(binding [foo 42] foo) ; 42

binding と let の違い

違いを見るために、まず foo の値を出力する簡単な関数を定義する。

(defn print-foo [] (println foo))

そして、let と binding でそれぞれ束縛を作り、関数を呼び出す。

(let [foo "let foo"] (print-foo))
;; 10
(binding [foo "binding foo"] (print-foo))
;; binding foo

let は自分のフォームの外側には影響を及ぼさないので、print-foo はルート束縛である 10 を表示する。一方、binding の束縛はそのボディから呼び出されるすべてのコードに及ぶので、束縛した値が表示される。

離れたところから影響を及ぼす

動的束縛は強力である反面、関数が純粋でなくなるという大きなデメリットを持っているので、基本的には使わない。

スペシャル変数と動的束縛

動的束縛を持たせるための var はスペシャル変数と呼ばれる。慣習的に、スペシャル変数は変数名の最初と最後にアスタリスク(*)を付ける。たとえば Clojure は標準入出力ストリーム *in*、*out*、*err* をスレッドごとに設定するのに動的束縛を使っている。

動的束縛は離れたところから影響を及ぼすことを可能にする。動的束縛を変更すると、遠くの関数の動作を、引数を変えることなく変えられる。

アスペクト指向プログラミング

離れたところから及ぼしたい影響の例として、一時的に関数に機能を付け加えるケースがある。いくつかの言語はそのための特別な機能を備えており、アスペクト指向プログラミング(AOP)と呼ばれたりする。Clojure におけるそういった機能は、単に動的束縛の副産物にすぎない。

サンプルコード(コストの高い計算)

例として、非常にコストの高い計算をする関数があるとする。これをシミュレートするために、次の関数を実装する。

(defn ^:dynamic slow-double [n]
  (Thread/sleep 100)
  (* n 2))

次に、ベクタの各要素に slow-double を適用する関数を実装する。

(defn calls-slow-double []
  (map slow-double [1 2 1 2 1 2]))

この関数の実行時間を計測する。そのままだと遅延シーケンスが返ってくるので、dorun で現実化する必要がある。

(time (dorun (calls-slow-double)))
;; "Elapsed time: 628.584458 msecs"

この関数が遅いのは、同じ処理を何度も行っているからである。

メモ化(memoize)

このようなケースでは、メモ化が真っ先に思いつく選択肢である。関数をメモ化すると、関数は過去の入力とその結果とのマップをキャッシュする。入力がキャッシュにヒットしたら、結果はほぼ瞬時に得られる。Clojure には、関数を受け取ってメモ化したバージョンを返す memoize という関数がある。

(memoize function)

サンプルコード(メモ化と動的束縛)

メモ化はよい選択肢だが、calls-slow-double は既に遅い(メモ化されていない)関数を呼んでしまっている。このような場合に動的束縛が役立つ。メモ化バージョンの slow-double を束縛して、その binding の中で calls-slow-double を呼び出せばよい。

(defn demo-memoize []
  (time
   (dorun
    (binding [slow-double (memoize slow-double)]
      (calls-slow-double)))))

動作を確かめると、3 倍近く速くなっている。

(demo-memoize)
;; "Elapsed time: 209.873375 msecs"

使用上の注意

うまく使えば動的束縛は素晴らしい力を発揮するが、コードの拡張や再利用の主要なメカニズムとして使うべきではない。動的束縛を使った関数は純粋ではなく、Clojure の関数型スタイルの利点を簡単に失ってしまう。

Java のコールバック API を使う

Java の API には、イベントハンドラをコールバックするように設計されているものがある。たとえば、Swing のような GUI フレームワークは、ユーザの入力に対応するためにイベントハンドラを呼び出す。

これらのコールバックハンドラのインタフェースは、ミュータブルなオブジェクトであることを前提に設計されている。また、シングルスレッドで動作することも多い。Clojure でこのような API を使う場合は、動的束縛を使うのがよい。以降はミュータブルな変数を使っているように見えるが、シングルスレッドであるという前提があるので、並行プログラミングにおける問題は生じない。

set! マクロ

set! は、スレッドローカルの束縛を変更するマクロである。

(set! var-symbol new-value)

このマクロは極力使うべきではない。 実際、Clojure のコアのソースではほとんど使われておらず、例外は SAX の ContentHandler の実装くらいである。

ContentHandler

ContentHandler は、パーサが XML のさまざまなコンポーネントに出くわしたときにコールバックを受ける。少し複雑なケースになると、ContentHandler は現在 XML 中のどこにいるか(現在パース中の要素のスタック、文字データなど)という情報を管理する必要がある。

Clojure の世界で考えるならば、ContentHandler が管理する現在の場所はイミュータブルな XML ストリームの特定の場所を指す変更可能なポインタであると言える。また、すべて 1 つのスレッドで処理されるので、ref を使う必要はない。次に実際のコードの一部を示す。

(startElement [uri local-name q-name ^Attributes atts]
	 ;; ... 省略 ...
   (set! *stack* (conj *stack* *current*))
   (set! *current* e)
   (set! *state* :element))
 nil)
(endElement [uri local-name q-name]
 (push-chars)
 (set! *current* (push-content (peek *stack*) *current*))
 (set! *stack* (pop *stack*))
 (set! *state* :between)
 nil)

SAX パーサは XML の開始タグに出くわすと startElement を呼ぶ。このコールバックハンドラは 3 つのスレッドローカル変数を更新する。

  • *stack*: すべての要素のスタック
  • *current*: 現在の要素
  • *state*: 要素の中身がどういう種類かを表す値

endElement は startElement の処理を巻き戻す。つまり、*stack* を pop し、先頭要素を *current* に置く。

キーワード