2010-07-16 113 views
26

我发现自己最近在clojure代码中使用了下面的习惯用法。clojure全局变量的最佳实践(参考vs alter-var-root)?

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

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

(defn update-global-var [val] 
    (dosync (ref-set *global-var* val))) 

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

回答

21

你的函数有副作用。使用相同的输入调用它们两次可能会给出不同的返回值,具体取决于*some-global-var*的当前值。这使得事情难以测试和推理,特别是一旦你有不止一个这样的全球变量浮动。

调用你的函数的人可能甚至不知道你的函数取决于全局变量的值,而不检查源。如果他们忘记初始化全局变量会怎么样?很容易忘记。如果你有两套代码都试图使用一个依赖这些全局变量的库?除非您使用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:只需将想要的“模拟”地址簿传入并查看它打印的内容即可。您不必关心任何全局状态或除目前正在测试的功能外的其他任何状态。

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

+6

我对你描述的功能方法并不陌生。但是,有时候,这种状态的全球位置的便利性是有用的。 所有的功能方法在边缘最常见的情况是IO。你可以认为这是IO的一个特例,因为它对所有线程都是有效的。 不要误解我的意思我更喜欢功能性的方法,而我上面参考的例子的用法是过于简单的,所以我大部分都认同你。 – 2010-07-17 03:27:03

+0

将值传递给所有的函数肯定是一个很好的选择,但是我觉得有时一个全局变量只是反复地将一堆价值传递给一堆函数而已。这是对副作用的品味和容忍的问题。 – ChrisBlom 2014-02-18 20:00:33

26

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

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

(def state (atom {})) 

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

(defn update-state [key val] 
    (swap! state assoc key val)) 
+0

这是接近我的首选方法,我做同样的购买使用defonce宣布共享位置,以避免覆盖它,并使用名称中的星号清楚它是一个共享位置 – ChrisBlom 2014-02-18 20:06:45

+0

在符号中使用星号名称现在会导致编译器警告,因为它们是为动态变量保留的。 – spieden 2016-12-13 22:21:00