使用闭包代替全局变量

dav*_*ugh 0 closures global-variables common-lisp

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

* (defparameter *x* 0)
*X*
* (defun foo ()
    (incf *x*))
FOO
* (defun bar ()
    (foo))
BAR
* (bar)
1
* *x*
1
Run Code Online (Sandbox Code Playgroud)

这似乎违反了参照透明性,因为它(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
Run Code Online (Sandbox Code Playgroud)

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

Rai*_*wig 8

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

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

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

没有普遍要求程序是引用透明的

通过软件工程原理指导软件设计非常有用,但是通常,简单的调试和维护比严格的原则更为重要。

(let ((x 0))
  (defun inc-x () (incf x))
  (defun reset-x () (setf x 0))
  (defun get-x () x))
Run Code Online (Sandbox Code Playgroud)

实际上以上意味着

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


cor*_*ump 5

Referential transparency means that if you bind some variable x to an expression e, you can replace all occurrences of x by e without changing the outcome. For example:

(let ((e (* pi 2)))
  (list (cos e) (sin e)))
Run Code Online (Sandbox Code Playgroud)

The above could be written:

(list (cos (* pi 2))
      (sin (* pi 2)))
Run Code Online (Sandbox Code Playgroud)

The resulting value is equivalent to the first one for some useful definition of equivalence (here equalp, but you could choose another one). Contrast this with:

(let ((e (random))
  (list e e))
Run Code Online (Sandbox Code Playgroud)

Here above, each call to random gives a different result (statistically), and thus the behaviour is different if you reuse the same result multiple times or generate a new after each call.

Special variables are like additional arguments to functions, they can influence the outcome of a result simply by being bound to different values. Consider *default-pathname-defaults*, which is used to build pathnames.

In fact, for a given binding of that variable, each call to (merge-pathnames "foo") returns the same result. The result changes only if you use the same expression in different dynamical context, which is no different than calling a function with different arguments.

The main difficulty is that the special variable is hidden, i.e. you might not know that it influences some expressions, and that's why you need them documented and limited in number.

What breaks referential transparency is the presence of side-effects, whether you are using lexical or special variables. In both cases, a place is modified as part of the execution of the function, which means that you need to consider when and how often you call it.

You could have better suggestions if you explained a bit more how your code is organized. You said that you have many special variables due to prototyping but in the refactoring you want to do it seems as-if you want to keep to prototypal code mostly untouched. Maybe there is a way to pack things in a nice modular way but we can't help without knowing more about why you need many special variables, etc.