【问题标题】:Best Practice for globals in clojure, (refs vs alter-var-root)?clojure 中全局变量的最佳实践(refs vs alter-var-root)?
【发布时间】:2011-03-17 02:58:43
【问题描述】:

我发现自己最近在 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 不仅仅适用于线程代码,而且基本上适用于任何需要不变性的全局。有更好的做法吗?我可以尝试重构代码以仅使用 binding 或 let,但这对于某些应用程序可能会变得特别棘手。

【问题讨论】:

    标签: clojure globals refs


    【解决方案1】:

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

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

    (def state (atom {}))
    
    (defn get-state [key]
      (@state key))
    
    (defn update-state [key val]
      (swap! state assoc key val))
    

    【讨论】:

    • 这接近我的首选方法,我做同样的购买使用 defonce 来声明共享位置,以避免覆盖它,并在名称中使用星号来明确它是一个共享位置
    • 在符号名称中使用星号现在会导致编译器警告,因为这些是为动态变量保留的。
    【解决方案2】:

    你的函数有副作用。根据*some-global-var* 的当前值,使用相同的输入调用它们两次可能会给出不同的返回值。这使得事情难以测试和推理,尤其是当您有多个这些全局变量在浮动时。

    调用您的函数的人可能甚至不知道您的函数取决于全局 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 或全局变量。

    【讨论】:

    • 我对您描述的功能方法并不陌生。但有时保持状态的全局位置的便利性是有用的。所有功能性方法都在边缘崩溃,最常见的情况是 IO。您可以将其视为 IO 的一种特殊情况,因为它实际上对所有线程都是全局的。不要误会我的意思,我更喜欢函数式方法,我上面对 ref 的示例使用过于简单,所以我在很大程度上同意你的看法。
    • 将值传递给所有函数当然是一个不错的选择,但我觉得有时全局变量比一遍又一遍地将一堆值传递给一堆函数更实用。这是口味和对副作用的耐受性问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-12-19
    • 2018-04-18
    • 1970-01-01
    • 1970-01-01
    • 2016-07-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多