在学习函数式编程时,我不断遇到“原因”一词,尤其是在纯函数和/或引用透明性的上下文中。有人可以解释这到底是什么意思吗?
7 回答
通常,在编写程序时,您的工作不会仅仅以编写代码而告终,您还想知道代码表现出的一些属性。您可以通过两种方式得出这些属性:通过逻辑分析或经验观察。
此类属性的示例包括:
- 正确性(程序做它应该做的)
- 性能(需要多长时间)
- 可扩展性(性能如何受输入影响)
- 安全性(算法是否会被恶意滥用)
当您凭经验测量这些属性时,您会得到精度有限的结果。因此,从数学上证明这些性质要优越得多,但并不总是那么容易做到。函数式语言通常将使其属性的数学证明作为其设计目标之一。这就是推理程序的典型含义。
就功能或较小的单位而言,上述适用,但有时作者也只是意味着考虑算法或设计算法。这取决于特定的用法。
顺便说一句,一些例子说明了人们如何对其中一些事情进行推理,以及如何进行经验观察:
正确性:我们可以证明代码是正确的,如果我们可以用方程式证明它做了它应该做的事情。所以对于一个排序函数,如果我们可以证明我们给它的任何列表都具有被排序的属性,我们就知道我们的代码是正确的。根据经验,我们可以创建一个单元测试套件,在其中我们提供输入代码示例并检查代码是否具有所需的输出。
性能和可扩展性:我们可以分析我们的代码并证明算法的性能界限,以便我们知道它所花费的时间如何取决于输入的大小。根据经验,我们可以对我们的代码进行基准测试,看看它在特定机器上的实际运行速度。我们可以执行负载测试并查看我们的机器/算法在折叠/变得不切实际之前可以接受多少实际输入。
对代码进行推理,从最松散的意义上来说,意味着思考你的代码以及它的真正作用(而不是你认为它应该做的事情。)这意味着
- 当你向它抛出数据时,意识到你的代码的行为方式,
- 知道哪些东西可以在不破坏的情况下进行重构,并且
- 密切关注可以执行哪些优化,
除其他事项外。对我来说,在调试或重构时,推理部分起着最大的作用。
举一个你提到的例子:当我试图找出一个函数有什么问题时,引用透明度对我有很大帮助。引用透明性保证了当我使用函数时,给它不同的参数,我知道函数会在我的程序中以相同的方式做出反应。它不依赖于它的论点以外的任何东西。这使得函数更容易推理——与命令式语言相反,在命令式语言中,函数可能依赖于一些外部变量,而这些外部变量会在我的眼皮子底下发生变化。
另一种看待它的方式(这在重构时更有帮助)是你越了解你的代码满足某些属性,它就越容易推理。例如,我知道
map f (map g xs) === map (f . g) xs
这是一个有用的属性,我可以在重构时直接应用。我可以陈述 Haskell 代码的这些属性这一事实使推理更容易。我可以尝试在 Python 程序中声明这个属性,但我对它的信心会大大降低,因为如果我的选择不走运,f
结果g
可能会有很大差异。
非正式的意思是,“能够通过查看代码来判断程序将要做什么。” 由于副作用、强制转换、隐式转换、重载函数和运算符等,这在大多数语言中可能会非常困难。也就是说,当您无法仅使用大脑来推理代码时,您必须运行它以查看它适用于给定的输入。
通常当人们说“推理”时,他们的意思是“等式推理”,这意味着在不运行代码的情况下证明代码的属性。
这些属性可以非常简单。例如,给定(.)
and的以下定义id
:
id :: a -> a
id x = x
(.) :: (b -> c) -> (a -> b) -> (a -> c)
(f . g) = \x -> f (g x)
...然后我们可能想证明:
f . id = f
这很容易证明,因为:
(f . id) = \x -> f (id x) = \x -> f x = f
请注意我是如何为所有人 f
证明这一点的。这意味着我知道这个属性无论如何总是正确的,因此我不再需要在某种单元测试套件中测试这个属性,因为我知道它永远不会失败。
“推理程序”只是“分析程序以查看它的作用”。
这个想法是纯度简化了理解,无论是通过人类更改程序,还是通过机器编译程序或分析程序以找出破碎的极端情况。
这个问题的许多正确答案已经给出,指的是正确性的数学证明。但我希望能为不一定是数学专业的程序员提供一个实用的答案。
正如 Douglas Crockford 所观察到的,正确性的正式证明在当代编程实践中并不重要。 [1]
在调试阶段,“关于你的代码的原因”这句话首先对我有实际意义。问题是:当事情出错时,您是否容易确定出错的原因?
如果每个函数的行为仅取决于其输入,那么预测函数体内会发生什么应该是相当简单的。意外错误意味着未处理某些输入案例。(例如,一个常见的问题是没有预料到null
争论。)
另一方面,如果一个函数的结果取决于该函数不拥有或控制的外部变量的状态,那么当错误发生时,很难追踪是什么导致了系统所处的状态。(解决这些问题是系统进行大量日志记录的动机。)
这就是说函数式风格允许你“推理”你的代码的原因。
- 它将可能出错的地方限制在函数参数与其返回值之间可能发生的情况。
- 它将可能出错的地方限制在函数拥有和控制的少数元素上。
而且,如果您知道在哪个函数中遇到了错误,那么您应该能够很容易地找出肯定出了什么问题,以及它是如何出现的。(并且,如有必要,将调用堆栈展开到必须进入意外值的地方。)
行为驱动开发范式也证明了“推理”:
给定:有限数量的可能初始条件。(例如,参数。)
何时:执行您定义的流程。
那么:可能的结果范围有限且已知。
简而言之,这就是“推理”代码。
(当然,这也取决于你的函数体不修改外部变量,这就是函数式程序员喜欢称之为“副作用”的东西。)
Edsger Dijkstra以反对该goto
声明而闻名。他推断,如果允许程序任意跳转到其定义中的任何一行,那么您就无法期望预测其运行时行为。
函数式编程范式更进一步:它希望将程序逻辑外部的任何状态的影响限制为实现其目的所必需的。
这样 - 当您调试错误时 - 只需阅读代码就足以了解其原因。
[1]:克罗克福德,道格拉斯。“测试如何工作”。Javascript 是如何工作的。Virgule-Solidus LLC,2018 年。EPUB。
正如@John Wiegley 所说,关于手段的原因
仅通过查看代码就能知道程序会做什么
更重要的是了解是什么阻碍了我们对代码进行推理。这些都是副作用。