我希望这不是在打败一匹死马,但我想就编写引用透明代码的另一种可能策略发表意见。(之前关于参照透明度的讨论见使用闭包而不是全局变量)。同样,目标是消除大多数全局变量,但保留它们的便利性,而无需将容易出错的引用或潜在的非功能行为(即,引用不透明性、副作用和不可重复的评估)注入代码。
建议使用局部特殊变量来建立初始绑定,然后可以动态地传递给最终使用它们的后续嵌套函数。与全局变量一样,预期的优点是局部特殊变量不需要作为参数通过所有中间函数传递(其功能与局部特殊变量无关)。但是,为了保持引用的透明度,它们将作为参数传递给最终的使用者函数。
我想知道的是,浮动大量动态变量是否容易出现编程错误。对我来说,这似乎不是特别容易出错,因为以前绑定的变量的任何本地重新绑定都不会影响原始绑定,一旦它被释放:
(defun main ()
(let ((x 0))
(declare (special x))
(fum)))
(defun fum ()
(let ((x 1)) ;inadvertant? use of x
(setf x 2))
(foo))
(defun foo ()
(declare (special x))
(bar x))
(defun bar (arg) ;final consumer of x
arg)
(main) => 0
这种策略有问题吗?
现在,您的函数引用了一个不保证定义的变量。尝试在 repl 处执行(foo)
将引发未绑定变量错误。不仅有引用不透明度,而且现在还有引用上下文错误抛出!
您在这里拥有的是全局绑定例程,这些例程只能在暗示(declare (special x))
的本地上下文中执行。您也可以将这些函数放在labels
中,这样它们就不会被意外使用,尽管此时您可以选择关闭函数中的变量或关闭函数中的函数:
(defun main ()
(labels ((fum ()
(let ((x 1));Inadvertent use of x?
(setf x 2))
(foo))
(foo ()
(declare (special x))
(bar x))
(bar (arg) arg)) ;Final consumer of x.
(let ((x 0))
(declare (special x))
(fum))))
哇,这是一些丑陋的代码!
卷积后,我们可以使x
词法化!现在我们可以实现圣杯,参考透明度!
卷曲
(defun main ()
(let ((x 0))
(labels ((fum ()
(let ((x 1))
(setf x 2))
(foo))
(foo () (bar x))
(bar (arg) arg));Final consumer of x.
(fum))))
这段代码要好得多,而且很活泼。它本质上是另一个问题的代码,但函数绑定是本地化的。这至少比使用爆炸性的全局命名要好。内在的让什么都不做,和以前一样。虽然现在它不那么复杂了。
CL-USER> (main) ;=> 0
您的测试用例在两者中(main) ;=> 0
相同。原则是仅按词法封装变量,而不是使用动态special
声明。现在,我们可以按照建议,通过在单个环境变量中以功能方式传递内容来进一步减少代码。
(defun convoluted-zero ()
(labels ((fum (x)
(let ((x 1))
(setf x 2))
(foo x))
(foo (x) (bar x))
(bar (arg) arg)).
(fum 0)))
CL-USER> (let ((x (convoluted-zero)))
(list x (convoluted-zero)))
;=> 0
□使用特殊变量对代码进行 QED违反了抽象。
如果你真的想掉进兔子洞,你可以阅读Doug Hoyte的Let Over Lambda第6章关于潘多拉宏的部分,在那里你可以做这样的事情:
(use-package :let-over-lambda)
(let ((c 0))
(setf (symbol-function 'ludicrous+)
(plambda () (c) (incf c)))
(setf (symbol-function 'ludicrous-)
(plambda () (c)(decf c))))
然后,您可以使用pandoric-get
来获取 c,而无需递增它或在该上下文中定义任何访问器函数,这绝对是疯狂的。使用 lisp 包,你可以摆脱包本地"全局"变量。例如,我可以在 elisp 中看到一个应用程序,它没有内置包。