関数には副作用があります。同じ入力でそれらを 2 回呼び出すと、 の現在の値に応じて異なる戻り値が返される場合があり*some-global-var*
ます。これにより、特にこれらのグローバル変数が複数ある場合は、テストと推論が難しくなります。
関数を呼び出す人は、ソースを検査しないと、関数がグローバル var の値に依存していることさえ知らない場合があります。グローバル変数を初期化するのを忘れた場合はどうなりますか? 忘れがちです。これらのグローバル変数に依存するライブラリを使用しようとする 2 つのコード セットがある場合はどうなるでしょうか? を使用しない限り、それらはおそらく互いに重なり合いますbinding
。また、参照からデータにアクセスするたびにオーバーヘッドが追加されます。
副作用のないコードを書けば、これらの問題はなくなります。関数は独立しています。テストは簡単です。いくつかの入力を渡し、出力を検査すると、常に同じになります。関数がどの入力に依存しているかは簡単にわかります。それらはすべて引数リストにあります。これで、コードはスレッドセーフになりました。そして、おそらくより速く実行されます。
「一連のオブジェクト/メモリを変更する」スタイルのプログラミングに慣れている場合、このようにコードを考えるのは難しいですが、一度コツをつかめば、この方法でプログラムを編成するのは比較的簡単になります。コードは通常、同じコードのグローバル ミューテーション バージョンと同じか、それよりも単純になります。
これは非常に不自然な例です:
(def *address-book* (ref {}))
(defn add [name addr]
(dosync (alter *address-book* assoc name addr)))
(defn report []
(doseq [[name addr] @*address-book*]
(println name ":" addr)))
(defn do-some-stuff []
(add "Brian" "123 Bovine University Blvd.")
(add "Roger" "456 Main St.")
(report))
孤立して見るとdo-some-stuff
、一体何をしているのですか?暗黙のうちに起こっていることがたくさんあります。このパスの下には、スパゲッティがあります。間違いなくより良いバージョン:
(defn make-address-book [] {})
(defn add [addr-book name addr]
(assoc addr-book name addr))
(defn report [addr-book]
(doseq [[name addr] addr-book]
(println name ":" addr)))
(defn do-some-stuff []
(let [addr-book (make-address-book)]
(-> addr-book
(add "Brian" "123 Bovine University Blvd.")
(add "Roger" "456 Main St.")
(report))))
do-some-stuff
孤立していても、何が行われているかは明らかです。必要な数のアドレス帳を配置できます。複数のスレッドが独自のスレッドを持つことができます。このコードは、複数の名前空間から安全に使用できます。アドレス帳は引数として渡すので、忘れずに初期化してください。簡単にテストreport
できます。必要な「モック」アドレス帳を渡して、何が印刷されるかを確認するだけです。現在テストしている関数以外は、グローバルな状態などを気にする必要はありません。
複数のスレッドからのデータ構造への更新を調整する必要がない場合は、通常、ref またはグローバル変数を使用する必要はありません。