【问题标题】:Using a Closure instead of a Global Variable使用闭包代替全局变量
【发布时间】:2019-06-24 21:57:19
【问题描述】:

这个问题是 Using Local Special Variables 的 cmets 的延续,关于如何最好地避免全局变量。据我了解,全局变量是有问题的,主要是因为它们有可能干扰参考透明度。如果表达式使用其调用上下文之外的信息(例如,全局变量本身的先前值或任何其他外部值)来更改全局值,则违反透明度。在这些情况下,评估表达式可能会在不同的时间产生不同的结果,无论是返回的值还是副作用。 (然而,似乎并非所有全局更新都有问题,因为某些更新可能不依赖于任何外部信息——例如,将全局计数器重置为 0)。深度嵌入计数器的正常全局方法可能如下所示:

* (defparameter *x* 0)
*X*
* (defun foo ()
    (incf *x*))
FOO
* (defun bar ()
    (foo))
BAR
* (bar)
1
* *x*
1

这似乎违反了引用透明性,因为(incf *x*) 依赖于*x* 的外部(全局)值来完成其工作。以下是通过消除全局变量来保持功能和引用透明性的尝试,但我不相信它确实如此:

* (let ((x 0))
    (defun inc-x () (incf x))
    (defun reset-x () (setf x 0))
    (defun get-x () x))
GET-X
* (defun bar ()
    (inc-x))
BAR
* (defun foo ()
    (bar))
FOO
* (get-x)
0
* (foo)
1
* (get-x)
1

全局变量现在已经消失了,但表达式(inc-x) 似乎仍然具有(潜在的)副作用,并且每次调用它都会返回不同的(但未使用的)值。这是否证实了对相关变量使用闭包并不能解决透明度问题?

【问题讨论】:

    标签: closures global-variables common-lisp


    【解决方案1】:

    全局变量存在问题主要是因为它们有可能干扰引用透明度

    如果要创建全局配置值,Common Lisp 中的全局变量就可以了。

    通常需要打包一堆配置状态,然后将其放入一个对象中可能会更好。

    对于程序参照透明没有一般要求。

    通过软件工程原则来指导软件设计很有用,但通常易于调试和维护比严格的原则更重要。

    (let ((x 0))
      (defun inc-x () (incf x))
      (defun reset-x () (setf x 0))
      (defun get-x () x))
    

    实际上上面的意思是它

    • 难以检查
    • 重新加载代码会产生问题
    • 禁止文件编译器识别函数的顶级性质
    • 创建一个完整的 API 来管理单个变量

    【讨论】:

    • 很高兴听到关于全局变量的常识。猜猜你只需要一种平衡的方法来使用它们,而不是一概回避。
    • 如果您有很多具有结构的变量,建议将他的变量放入单个结构中(使用 CLOS)是一个很好的解决方案,而我建议在 alist 中包含较少结构化的全局信息的解决方案.适合您的需求。
    【解决方案2】:

    引用透明意味着如果将某个变量 x 绑定到表达式 e,则可以将所有出现的 x 替换为 e 而不会改变结果。例如:

    (let ((e (* pi 2)))
      (list (cos e) (sin e)))
    

    上面可以写成:

    (list (cos (* pi 2))
          (sin (* pi 2)))
    

    对于一些有用的等价定义,结果值等同于第一个值(此处为equalp,但您可以选择另一个)。对比一下:

    (let ((e (random))
      (list e e))
    

    在上面,对random 的每次调用都会给出不同的结果(统计上),因此如果您多次重复使用相同的结果或在每次调用后生成一个新结果,则行为会有所不同。

    特殊变量就像函数的附加参数,它们可以通过绑定到不同的值来影响结果的结果。考虑*default-pathname-defaults*,它用于构建路径名。

    事实上,对于该变量的给定绑定,对(merge-pathnames "foo") 的每次调用都会返回相同的结果。仅当您在不同的动态上下文中使用相同的表达式时,结果才会发生变化,这与调用具有不同参数的函数没有什么不同。

    主要困难在于特殊变量是隐藏的,即您可能不知道它会影响某些表达式,这就是为什么您需要记录它们并限制它们的数量。

    破坏引用透明度的是副作用的存在,无论您使用的是词法变量还是特殊变量。在这两种情况下,地方都会作为函数执行的一部分进行修改,这意味着您需要考虑何时以及多久调用它。

    如果您多解释一下代码的组织方式,您可能会得到更好的建议。您说由于原型设计,您有许多特殊变量,但在重构中,您似乎想要保持原型代码大部分不受影响。也许有一种方法可以以一种很好的模块化方式打包东西,但是如果不了解更多关于为什么需要许多特殊变量等,我们将无法提供帮助。

    【讨论】:

    • 有没有办法可以更直接地将您的 cmets 与我上面的 cmets 和问题联系起来?例如,哪些陈述是混乱/错误的?你提到的似乎与我的理解大体一致(尽管粗略)。
    【解决方案3】:

    该代码不是引用透明的。这是对special 变量的改进。

    如果您删除了reset-x,您输入的代码将是一个功能性的nonce

    我对@9​​87654322@ 的回答有关于special 变量的一般准则。对于您的具体情况,也许它们值得?例如,我可以看到将特殊变量用作随机数的情况,在这种情况下传递它们可能很愚蠢。

    Common Lisp 有很多处理全局信息的工具,所以很少需要有很多全局变量。您可以定义一个*env* alist 来存储您的值,或者将它们放入哈希表中,或者将它们放入符号 plist 中,或者将它们打包在闭包中以传递,或者做其他事情,或者使用 CLOS。

    【讨论】:

    • 但在我看来,您提到的 CL 全局“设施”都涉及创建另一个(异构)全局变量。即使您这样做,您是否仍然存在引用透明度问题,因为每个(现在更深地隐藏)更新都是涉及新全局变量的另一个副作用?参照透明性是否更像是一种理想而不是一种实用性?
    • 您确实仍然会遇到问题,尽管从隐藏该实现细节中进行更多抽象可能会更好。我提到了 nonce,因为它是这种抽象的一个很好的例子:虽然不透明,但它是功能性的和抽象的,所以它几乎一样好。
    • @davypough 如果您的问题是全局变量太多,那么使用更高级别的构造来打包它们,CLOS 或其他方式,是最直接的步骤!很少有人认为您应该摆脱 all 全局变量,但越少越好。太多的函数参数以同样的方式解决,考虑一个environment 变量。
    【解决方案4】:

    第二个例子的副作用在哪里? let 内的 x 无法从外部访问。

    这是另一个闭包示例,其中包含顶级函数和一个明确的计数器。

    (defun repeater (n)
      (let ((counter -1))
         (lambda ()
           (if (< counter n)
             (incf counter)
             (setf counter 0)))))
    
    (defparameter *my-repeater* (repeater 3))
    ;; *MY-REPEATER*
    (funcall *my-repeater*)
    0
    (funcall *my-repeater*)
    1
    

    https://lispcookbook.github.io/cl-cookbook/functions.html#closures

    【讨论】:

    • 是的,let 中的 x 不能从外部访问,但一个地方仍在修改中,不是吗?正如 coredump 所说,这种副作用破坏了引用透明度。根据 Rainer Joswig 的评论,您的闭包示例似乎是对我的改进,因为它只涉及一个匿名函数,而不是顶级函数。
    猜你喜欢
    • 2017-10-06
    • 1970-01-01
    • 2012-07-01
    • 1970-01-01
    • 2022-01-25
    • 2013-05-25
    • 2012-12-09
    • 1970-01-01
    • 2022-10-14
    相关资源
    最近更新 更多