在clojure中的全局变量的最佳实践,(refs vs alter-var-root)?

Jer*_*all 28 clojure globals refs

我发现自己最近在clojure代码中使用了以下习语.

(def *some-global-var* (ref {}))

(defn get-global-var []
  @*global-var*)

(defn update-global-var [val]
  (dosync (ref-set *global-var* val)))
Run Code Online (Sandbox Code Playgroud)

大多数情况下,这甚至不是多线程代码,可能需要refs给你的事务语义.它只是感觉refs不仅仅是线程代码,而且基本上适用于任何需要不变性的全局.这有更好的做法吗?我可以尝试重构代码以仅使用绑定或让但是对于某些应用程序来说这会变得特别棘手.

mik*_*era 30

当我看到这种模式时,我总是使用原子而不是参考 - 如果你不需要交易,只需要一个共享的可变存储位置,那么原子似乎就是要走的路.

例如,对于我将使用的键/值对的可变映射:

(def state (atom {}))

(defn get-state [key]
  (@state key))

(defn update-state [key val]
  (swap! state assoc key val))
Run Code Online (Sandbox Code Playgroud)


Bri*_*per 24

你的功能有副作用.使用相同的输入调用它们两次可以根据当前值给出不同的返回值*some-global-var*.这使得事情难以测试和推理,特别是一旦你有多个这样的全球变量浮出水面.

调用您的函数的人可能甚至不知道您的函数是否依赖于全局变量的值,而不检查源.如果他们忘记初始化全局变量怎么办?这很容易忘记.如果您有两组代码都试图使用依赖于这些全局变量的库,该怎么办?除非你使用,否则他们可能会互相踩到一边binding.每次从ref访问数据时,也会增加开销.

如果您自由编写代码副作用,这些问题就会消失.功能独立.它很容易测试:传递一些输入,检查输出,它们总是相同的.很容易看出函数所依赖的输入:它们都在参数列表中.现在你的代码是线程安全的.并且可能跑得更快.

如果你习惯于"改变一堆对象/内存"的编程风格,那么以这种方式思考代码是很棘手的,但是一旦掌握了它,就会以这种方式组织你的程序变得相对简单.您的代码通常最终会比同一代码的全局变异版本简单或简单.

这是一个非常人为的例子:

(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))
Run Code Online (Sandbox Code Playgroud)

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))))
Run Code Online (Sandbox Code Playgroud)

现在很清楚do-some-stuff正在做什么,即使是孤立的.您可以根据需要随意放置任意数量的地址簿.多个线程可以有自己的.您可以安全地使用来自多个名称空间的代码.您不能忘记初始化地址簿,因为您将其作为参数传递.您可以report轻松测试:只需传递所需的"模拟"地址簿,然后查看其打印内容.除了目前正在测试的功能之外,您不必关心任何全局状态或任何其他状态.

如果您不需要协调来自多个线程的数据结构的更新,则通常不需要使用ref或全局变量.

  • 我对你描述的功能方法并不陌生.但有时,保持状态的全局位置的便利性是有用的.所有功能方法都在最常见的IO边缘处发生故障.您可以将此视为IO的一个特例,因为它对所有线程都是有效的全局.不要误解我的意思我更喜欢功能方法,而我上面的例子使用的是一个过于简单的例子,所以我在很大程度上同意你的看法. (6认同)