When to use alter-var-root

Clojure を書いてる人であれば一度は alter-var-root を見たことがあると思いますが、 実際のアプリケーション開発においてこの関数を使うベストなタイミングがいつなのか分からないというのは初心者あるあるだと思います。

そもそも使った方がいいのか、使う機会があるのか、その辺から疑問だと思いますが、そのあたりについて少し書いておきましょう。

結論

本来的には使わないでもほとんどの場合は開発できるはず。

滅多なことがなければ必要になることもないです。

ではなんのためにあるのか

日本語で alter-var-root に触れている記事は、どれももあまりユースケースの話をしていなかったような気がします。

「再定義できます」、「 alter-var-root はアトミックな操作です」といった説明が書いてあるばかりで、 どういうときにどう使うと良いのかいまいち分からないままです。

ちなみに The Clojure Style Guide にも 再定義する場合は def ではなく alter-var-root を利用するようにと書いてあるわけですが、 そもそもどういったときに Var を再定義したくなるのか分からないと Var を変更したいときは alter-var-root を使えばいいんだという 大雑把な理解にしかならないと思います。

なので今回はどういったときに Var を再定義したくなるのかというところから説明していきたいと思います。

Var を再定義したくなるのはどんなときか

Clojure に慣れていないという前提であれば、考えられるとしたら以下のようなシーンでしょうか

  • カウンタなどで内部的に保持したいデータがあって、それを更新したい
  • ネームスペースの上部で定義した値を下部で再定義したい
  • 違うネームスペースの関数をテスト用に置き換えたい

カウンタなどのデータを保持したい場合

Var を再定義するスタイルで書くなら以下のようなコードを書くことが出来ます。

(def value 0)

(defn counter []
  ;; 良くないコード例なので真似しないでください
  (def value (inc value))
  value)

(counter) ;; => 1
(counter) ;; => 2
(counter) ;; => 3
(counter) ;; => 4
(counter) ;; => 5

このコードが良くない理由は次のテストで簡単に確かめることが出来ます。

(pmap #(do % (counter)) (range 100))
value ;; => 89 / 必ずこの値になるわけではありません

Clojure ではもしこのようなコードを書きたくなったら alter-var-root ではなく、 Ref, Agent, Atom などの参照型を利用します。

今回は Atom を使った例を示しておきます。

(def value (atom 0))

(defn counter []
  (swap! value inc))

(pmap #(do % (counter)) (range 100))

@value ;; => 100

ネームスペース内で値を再定義したい

これについてあまり良い例が思い浮ばないんですが、他でも書かれているように def 自体がアトミックな操作ではないため alter-var-root を利用しておく方がいいでしょう。

とはいえ、一般的に次のようなコードが登場することはあまりありません(そもそも別の名前を付ける方が適切だと思われるため)。

(def value 10)

(def value (* value value))

違うネームスペースの関数をテスト用に置き換えたい

例えば次のような関数があったとして関数 f をテストのときに書き換えたい場合、次のように再定義を行なうことができます。

(ns demo.core)

(defn- f [x] (+ x 1))

(defn g [x] (f x))
(ns demo.core-test
  (:require [demo.core :as c]
            [clojure.test :refer :all]))

(alter-var-root #'c/f (constantly identity))

(deftest g-test
  (is (= (c/g 10) 10)))

しかし、この場合は with-redefs などを利用するのが適当です。

(deftest g-test
  (with-redefs [c/f identity]
    (is (= (c/g 10) 10))))

alter-var-root を利用して関数を再定義してしまうと全ての利用箇所に影響が出てしまうため 開発しながらテストを同一 REPL 上で実行するということをした場合に不都合な事が起こります。

実際の利用シーン

ここまで「こういうときは alter-var-root を使うべきではない」という話をしてきました。 最後に実際の利用シーンを説明しようと思います。

グローバルな状態をアトミックに切り替えたいとき

次の例は Component のシステムを起動/停止するコードです。

(ns user
  (:require [com.stuartsierra.component :as component]
            [clojure.tools.namespace.repl :refer (refresh)]
            [examples :as app]))

(def system nil)

(defn init []
  (alter-var-root #'system
                  (constantly (app/example-system {:host "dbhost.com" :port 123}))))

(defn start []
  (alter-var-root #'system component/start))

(defn stop []
  (alter-var-root #'system
                  (fn [s] (when s (component/stop s)))))

systemalter-var-root によって変更しています。 Component の例や実際のコードではよく alter-var-root が登場します。 このとき単にアトミックな更新を行ないたいなら、 Atom などでも良いように思えますが Atom の場合リトライが走る可能性があるという点で異なります。

外部ライブラリの挙動をハックしたいとき

これが一番現実に良くあるのかなという気がしていますが…。

次のコードは Ring ミドルウェアのひとつをハックしたものですが、外部ライブラリの挙動をこのように変更してしまうことができます。

(defn alt-form-decode-str [f]
  (fn [^String encoded & [encoding]]
    (try
      (let [codec (URLCodec. (or encoding "UTF-8"))]
        (.decode codec encoded))
      (catch Exception _ nil))))

(alter-var-root #'codec/form-decode-str alt-form-decode-str)

外部のライブラリをハックしたいシーンはまあまあある( fork するほどではないとか、 PR したけど待ってられないとか)と思うので覚えておくと良い小技のひとつです。

まとめ

ここまでで述べた通り alter-var-root は頻繁に使うものではありませんが、覚えておくと Clojure でアプリを書いていてどうしようもなくなった場合などに役に立ちます。

健全な alter-var-root 生活を!