我发现自己最近在clojure代码中使用了以下习语。
(def *some-global-var* (ref {}))
(defn get-global-var []
@*global-var*)
(defn update-global-var [val]
(dosync (ref-set *global-var* val)))
大多数情况下,这甚至不是多线程代码,可能需要refs给你的事务语义。它只是感觉refs不仅仅是线程代码,而且基本上适用于任何需要不变性的全局。这有更好的做法吗?我可以尝试重构代码以仅使用绑定或允许但是对于某些应用程序来说这会变得特别棘手。
答案 0 :(得分:28)
当我看到这种模式时,我总是使用原子而不是参考 - 如果你不需要交易,只需要一个共享的可变存储位置,那么原子似乎就是这样。
e.g。对于我将使用的键/值对的可变映射:
(def state (atom {}))
(defn get-state [key]
(@state key))
(defn update-state [key val]
(swap! state assoc key val))
答案 1 :(得分:22)
您的功能有副作用。使用相同的输入调用它们两次可能会给出不同的返回值,具体取决于*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))
孤立地看着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
:只需传递所需的“模拟”地址簿并查看其打印内容。除了目前正在测试的功能外,您不必关心任何全局状态或其他任何状态。
如果您不需要协调来自多个线程的数据结构的更新,通常不需要使用refs或全局变量。