声明Lisp函数“纯粹”的能力是否有益?

时间:2011-08-31 08:38:03

标签: lisp computer-science compiler-optimization proof purely-functional

我最近一直在阅读很多关于Haskell的内容,以及它作为纯粹功能语言所带来的好处。 (我对讨论Lisp的monad不感兴趣)对我来说(至少在逻辑上)尽可能地隔离具有副作用的函数是有意义的。我已经充分使用了setf和其他破坏性函数,并且我认识到在Lisp和(大多数)它的衍生物中需要它们。

我们走了:

  1. (declare pure)这样的东西可能有助于优化编译器吗?或者这是一个有争议的问题因为它已经知道了吗?
  2. 声明是否有助于证明函数或程序,或者至少是声明为纯粹的子集?或者这又是不必要的东西,因为它对程序员和编译器以及证明者来说已经很明显了吗?
  3. 如果没有别的,编程人员是否会对使用此声明的函数强制执行纯度并增加Lisp程序的可读性/可维护性是否有用?
  4. 这有什么意义吗?或者我太累了,甚至不想现在呢?
  5. 我很欣赏这里的任何见解。有关编译器实现或可证明性的信息是受欢迎的。

    修改

    为了澄清,我不打算将这个问题限制在Common Lisp中。它显然(我认为)不适用于某些衍生语言,但我也很好奇其他Lisps的某些功能是否倾向于支持(或不支持)这种设施。

3 个答案:

答案 0 :(得分:7)

你有两个答案,但都没有触及真正的问题。

首先,是的,知道一个函数是纯粹的,显然会很好。有很多编译器级别的东西需要知道,以及用户级别的东西。鉴于lisp语言非常灵活,你可以稍微扭曲一下:而不是要求编译器更努力地尝试的“纯”声明,你只需要声明限制定义中的代码。这样你就可以保证函数是纯粹的。

你甚至可以通过其他支持工具来实现这一点 - 我在johanbev的回答中提到了其中两个:添加了不可变绑定和不可变数据结构的概念。我知道在 Common Lisp中这些是非常有问题的,特别是不可变的绑定(因为CL通过“副作用”加载代码到位)。但是这些功能将有助于简化事情,并且它们并不是不可思议的(例如,参见具有不可变对和其他数据结构的Racket实现,并且具有不可变的绑定。

但真正的问题是你能在这种有限的功能中做些什么。即使一个非常简单的问题也会出现问题。 (我正在使用类似Scheme的语法。)

(define-pure (foo x)
  (cons (+ x 1) (bar)))

似乎很容易告诉这个功能确实是纯粹的,它什么都不做。此外,似乎define-pure限制主体并且仅允许纯代码在这种情况下可以正常工作,并且将允许此定义。

现在从问题开始:

  1. 它正在调用cons,所以它假设它也是纯粹的。另外,正如我上面提到的,它应该依赖cons它是什么,所以假设cons绑定是不可变的。很容易,因为它是一个已知的内置。当然,对bar也这样做。

  2. 但是cons 会产生副作用(即使你在讨论Racket的不可变对):它分配一对新的。这似乎是一个次要且可忽略的观点,但是,例如,如果你允许这些东西出现在纯函数中,那么你将无法自动记忆它们。问题是,有人可能会依赖每个foo次来回复新对的呼叫 - 一个不是 - eq对任何其他现有对。看起来要做得很好,你需要进一步限制纯函数,不仅要处理不可变值,还要处理构造函数不总是创建新值的值(例如,它可以使用hash-cons而不是allocate)。

  3. 但是该代码也调用bar - 所以不需要对bar做出相同的假设:它必须被称为纯函数,具有不可变的绑定。特别注意bar没有接收参数 - 因此在这种情况下,编译器不仅要求bar是纯函数,它还可以使用该信息并预先计算其值。毕竟,没有输入的纯函数可以简化为普通值。 (注意BTW,Haskell没有零参数函数。)

  4. 这带来了另一个重大问题。如果bar一个输入的函数怎么办?在这种情况下,你会有一个错误,并会抛出一些异常......而且这不再是纯粹的。例外是副作用。除了其他所有内容之外,您现在还需要知道bar的arity,并且您需要避免其他异常。现在,输入x怎么样 - 如果它不是数字会怎样?这也会引发异常,所以你也需要避免它。这意味着您现在需要一个类型系统。

  5. (+ x 1)更改为(/ 1 x),您可以看到您不仅需要一个类型系统,还需要一个足够复杂以区分0的系统。

  6. 或者,您可以重新思考整个事情,并拥有永远不会抛出异常的新纯算术运算 - 但是除了所有其他限制之外,您现在离家很远,使用的语言是完全不同。

  7. 最后,还有一个副作用仍然是PITA:如果bar的定义是(define-pure (bar) (bar))怎么办?根据上述所有限制,这当然是纯粹的......但是分歧是副作用的一种形式,所以即使这不再是犹太教。 (例如,如果你确实让你的编译器优化了nullary函数的值,那么对于这个例子,编译器本身会陷入无限循环。)(是的,Haskell没有处理它,它没有成功少了一个问题。)

答案 1 :(得分:3)

给定一个Lisp函数,一般来说,知道它是否纯粹是不可判定的。当然,可以在编译时测试必要条件和充分条件。 (如果根本没有不纯的操作,那么函数必须是纯的;如果无条件的操作被无条件地执行,那么函数必须是不纯的;对于更复杂的情况,编译器可能试图证明函数是纯的或不纯的,但在所有情况下都不会成功。)

  1. 如果用户可以手动将函数注释为纯函数,那么编译器可以(a。)更加努力地证明函数是纯的,即。在放弃之前花费更多时间,或者(b。)假设它是并且添加对于不纯函数(例如,记忆结果)不正确的优化。所以,是的,如果假设注释是正确的,那么将函数注释为pure可以帮助编译器。

  2. 除了上面提到的“更加努力”的启发之外,注释无助于证明内容,因为它没有向证明者提供任何信息。 (换句话说,证明者可以假设注释在尝试之前总是在那里。)但是,将纯粹的函数附加到纯度的证明是有意义的。

  3. 编译器可以(a。)检查纯函数在编译时是否确实是纯的,但这通常是不可判定的,或者(b。)添加代码以尝试在运行时捕获纯函数中的副作用并将这些报告为错误。 (a。)可能对简单的启发式方法有帮助(比如“无条件的操作无条件执行”),(b。)对调试很有用。

  4. 不,这似乎有道理。希望这个答案也可以。

答案 2 :(得分:1)

当我们可以假设纯洁和参考时,通常的好东西适用 透明度。我们可以自动记忆热点。我们可以 自动并行化计算。我们可以解决很多问题 竞争条件。我们也可以使用我们的数据进行结构共享 知道不能修改,例如(准)原始``cons()'' 不需要复制它所涉及的列表中的cons-cells。 通过使用另一个cons-cell,这些细胞不会受到任何影响 指着它。这个例子有点明显,但编译器经常出现 在确定更复杂的结构共享方面表现良好。

然而,实际上确定一个lambda(一个函数)是纯的还是有的 Common Lisp中的引用透明度非常棘手。记住这一点 一个funcall(foo bar)从查看(symbol-function foo)开始。所以 这种情况

(defun foo (bar)
  (cons 'zot bar))

foo()是纯粹的。

下一个lambda也是纯粹的。

(defun quux ()
 (mapcar #'foo '(zong ding flop)))

然而,稍后我们可以重新定义foo:

(let ((accu -1))
 (defun foo (bar)
   (incf accu)))

下一次调用quux()不再是纯粹的!旧的纯foo()已经 重新定义为不纯洁的lambda。让人惊讶。这个例子可能有点 设计但是词汇重新定义一些并不常见 函数,例如let块。在那种情况下,它不是 可以知道在编译时会发生什么。

Common Lisp具有非常动态的语义,所以实际存在 能够提前确定控制流和数据流(for 编译时的实例)非常难,并且在大多数有用的情况下 完全不可判定的。这是非常典型的动态语言 类型系统。你不能使用Lisp中有很多常见的习语 如果你必须使用静态类型。主要是这些犯规 尝试做有意义的静态分析。我们可以为原语做到这一点 像缺点和朋友。但对于涉及其他事物的lambda 原始我们在更深的水中,特别是在那些情况下 我们需要看看功能之间复杂的相互作用。记住这一点 如果lambda所调用的所有lambda都是纯粹的,那么lambda就是纯粹的。

在我的头顶,有可能,有一些深层的宏观, 消除重新定义问题。从某种意义上说,每个lambda都会得到 一个额外的参数,它是一个代表整个状态的monad lisp图像(我们显然可以限制自己的功能 实际上会看)。但能做到这一点可能更有用 在我们承诺编译器的意义上,我们自己声明纯度 这个lambda确实是纯净的。如果不是那么后果 未定义,各种各样的混乱可能随之而来......