【问题标题】:Using a Local Special Variable Passed as a Final Argument使用作为最终参数传递的局部特殊变量
【发布时间】:2019-06-25 22:35:22
【问题描述】:

我希望这不是在打死马,但我想就编写引用透明代码的另一种可能策略发表意见。 (之前关于引用透明度的讨论在Using a Closure instead of a Global Variable)。同样,目标是消除大多数全局变量,但保留它们的便利性,而不会将容易出错的引用或潜在的非功能性行为(即,引用不透明、副作用和不可重复的评估)注入代码中。

建议是使用局部特殊变量来建立初始绑定,然后可以动态地将其传递给最终使用它们的后续嵌套函数。与全局变量一样,预期的优点是不需要将局部特殊变量作为参数传递给所有中间函数(其功能与局部特殊变量无关)。但是,为了保持引用透明性,它们将作为参数传递给最终消费者函数。

我想知道的是,浮动大量动态变量是否容易出现编程错误。对我来说,这似乎不是特别容易出错,因为以前绑定的变量的任何本地重新绑定都不应影响原始绑定,一旦它被释放:

(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

这个策略有问题吗?

【问题讨论】:

  • 你去恢复多年的 Common Lisp 开发(特殊变量把你的大脑变成布丁),然后你退缩并定义一个接受参数的函数?为什么要这么做?你的目标不是避免传递争论吗? 现在说真的 - 这个策略有问题吗? - 是的,很多,调试ie是一场噩梦。
  • TL;DR; 是的
  • 我试图指出局部特殊变量和引用透明度不一定不兼容。如果您的嵌套函数在引用上是透明的,那么您可以可靠地自下而上地测试它们,一次一个。没有复杂的调试,只需对每个都进行良好的简单测试。
  • 仅仅因为你的代码没有修改全局变量,并不意味着它是引用透明的。简而言之,这意味着函数只对自己的参数进行操作。你所做的恰恰相反。请不要通过阅读 On Lisp 的部分内容来开始学习 Lisp。从 Common Lisp:符号计算简介 开始。 On Lisp 绝不是教程。
  • @davypough 你应该看到 Doug Hoyte 的 Let Over Lambda 部分,pandoric macros in chapter 6 做你想做的事。我用一个例子编辑了我的答案。

标签: dynamic global-variables common-lisp


【解决方案1】:

现在您的函数正在引用一个不能保证定义的变量。尝试在 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 而不增加它或在该上下文中定义任何访问器函数,这绝对是疯狂的。使用 lisp 包,您可以摆脱包本地的“全局”变量。例如,我可以在 elisp 中看到一个应用程序,它没有内置任何包。

【讨论】:

  • 很好的解释——感谢你和我一起经历这个!我想我的收获是首先将单个全局变量捆绑到某种全局容器对象中。从技术上讲,它不提供参考透明度,但它至少提供了一层保护,防止意外修改。
  • 是的,好计划。您可以使用不可变的全局对象来获得透明度,就像在 Clojure 中一样。 @davypough
  • 期待 pandoric 宏——谢谢!这超出了我的想象,但 CL 的抽象令人着迷。
  • @davypough 他们在你头上的事实说明了为什么,在我看来,他们是一个坏主意:他们比仅仅使用包局部变量(即全局)复杂得多。
  • 当然,但我敢打赌它们非常适合一些晦涩的编程情况。此外,探索 CL 很有趣;)你真的帮了大忙!
猜你喜欢
  • 1970-01-01
  • 2013-01-02
  • 1970-01-01
  • 2011-02-14
  • 1970-01-01
  • 1970-01-01
  • 2018-03-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多