使用闭包代替全局变量

时间:2019-06-24 21:57:19

标签: closures global-variables common-lisp

这个问题是Using Local Special Variables上有关如何最好地避免全局变量的注释的继续。据我了解,全局变量存在问题,主要是因为它们有可能干扰参照透明性。如果表达式使用其调用上下文之外的信息来更改全局值(例如,全局变量本身的先前值或任何其他外部值),则会违反透明度。在这些情况下,评估表达式在不同时间可能有不同的结果,无论是返回的值还是副作用。 (但是,似乎并不是所有的全局更新都是有问题的,因为某些更新可能不依赖于任何外部信息,例如,将全局计数器重置为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)似乎仍具有(潜在)副作用,并且每次调用它都会返回不同(但未使用)的值。这是否确认对相关变量使用闭包不能解决透明性问题?

4 个答案:

答案 0 :(得分:8)

  

全局变量之所以成问题,主要是因为它们有可能干扰参照透明性

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

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

没有普遍要求程序是参照透明

按照软件工程原理指导软件设计很有用,但是容易调试和维护通常比严格原则更重要。

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

实际上是上面的意思

  • 很难检查
  • 重新加载代码会产生问题
  • 禁止文件编译器识别功能的顶层性质
  • 创建一个仅用于管理单个变量的完整API

答案 1 :(得分:5)

引用透明性意味着,如果将某个变量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")的调用都会返回相同的结果。仅当您在不同的动态上下文中使用相同的表达式时,结果才会更改,这与调用具有不同参数的函数没有什么不同。

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

打破引用透明性的是副作用,无论您使用的是词法变量还是特殊变量。在这两种情况下,位置都会作为函数执行的一部分进行修改,这意味着您需要考虑何时调用频率

如果您进一步解释代码的组织方式,则可能会有更好的建议。您说过,由于 prototyping ,您有许多特殊变量,但是在重构中,您想做的似乎就像您想保持原型代码基本不变。也许有一种方法可以用一种很好的模块化方式打包东西,但是如果您不了解为什么需要很多特殊变量等信息,我们将无济于事。

答案 2 :(得分:1)

该代码不是参照透明的。不过,它是对special变量的改进。

如果您删除了reset-x,则放置的代码将是功能nonce

我对your previous question的回答具有关于special变量的一般准则。对于您的特定情况,也许值得吗?我可以看到使用特殊变量作为随机数的情况,例如,将它们传递到周围可能很愚蠢。

Common Lisp具有许多用于处理全局信息的功能,因此很少需要具有许多全局变量。您可以定义一个*env*列表来存储您的值,或将它们放在哈希表中,或将它们放入符号栈中,或将它们打包在一个闭包中以进行传递,或执行其他操作,或使用CLOS。

答案 3 :(得分:1)

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

这是另一个闭包示例,其中包含顶级功能,并且内部明确包含一个计数器。

(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