我希望这不会打败老手,但是我想对编写参照透明代码的另一种可能的策略发表意见。 (先前有关引用透明性的讨论在Using a Closure instead of a Global Variable中)。再次,目标是消除大多数全局变量,但保留其便利性,而无需在代码中注入容易发生错误的引用或潜在的非功能性行为(即,参照不透明,副作用和不可重复的评估)。>
建议使用局部特殊变量建立初始绑定,然后可以将其动态传递给最终使用它们的后续嵌套函数。像全局变量一样,预期的优点是不需要将局部特殊变量作为参数传递给所有中间函数(其功能与局部特殊变量无关)。但是,为了保持引用透明性,它们将作为参数传递给最终使用者函数。
我想知道的是,在周围浮动许多动态变量是否容易产生编程错误。我似乎不太容易出错,因为任何先前绑定的变量的本地重新绑定在释放后都不会影响原始绑定:
if (compare($one, $two)) {
// do something
}
这种策略有问题吗?
答案 0 :(得分:5)
现在,您的函数正在引用不能保证定义的变量。尝试在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))))
这段代码好得多,也很简单。它本质上是your code to the other question,但函数绑定已本地化。这至少比使用爆炸性全局命名更好。内部let不会执行任何操作,与以前一样。虽然现在不那么令人费解了。
CL-USER> (main) ;=> 0
您的测试用例在两者中都是相同的(main) ;=> 0
。原理是只按顺序封装变量,而不用动态的special
声明。现在,我们可以通过仅在单个环境变量as suggested中进行功能传递来进一步减少代码。
(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 您的带有特殊变量的代码违反了抽象。
如果您真的想钻进兔子洞,可以阅读section of chapter 6 of Doug Hoyte's Let Over Lambda on pandoric macros,在这里您可以执行以下操作:
(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,而无需递增c或在该上下文中定义任何访问器函数,这绝对是傻瓜。使用lisp软件包,您可以摆脱软件包本地的“全局”变量。例如,我可以在elisp中看到一个没有内置软件包的应用程序。