在学习函数式编程时,我不断遇到“理由”一词,特别是在纯函数和/或引用透明性的背景下。有人可以解释这究竟是什么意思吗?
答案 0 :(得分:44)
通常在编写程序时,您的工作不仅仅是编写代码,而且您还需要了解代码所展示的某些属性。您可以通过两种方式到达这些属性:通过逻辑分析或通过经验观察。
此类属性的示例包括:
根据经验测量这些属性时,会得到精度有限的结果。因此,从数学角度证明这些特性是非常优越的,但并不总是那么容易。功能语言通常具有其设计目标之一,使其属性的数学证明更易于处理。这就是通常对程序进行推理的意思。
就功能或较小单位而言,以上适用,但有时作者仅仅意味着考虑算法或设计算法。这取决于具体用途。
除此之外,还有一些例子说明人们如何推理这些事情,以及如何进行经验观察:
正确性:我们可以证明代码是正确的,如果我们能够公平地表明它做了它应该做的事情。因此,对于排序函数,如果我们可以显示我们给它的任何列表将具有被排序的属性,我们知道我们的代码是正确的。根据经验,我们可以创建一个单元测试套件,在其中我们提供输入的代码示例并检查代码是否具有所需的输出。
性能&可伸缩性:我们可以分析我们的代码并证明算法的性能界限,以便我们知道它所花费的时间取决于输入的大小。根据经验,我们可以对代码进行基准测试,看看它在特定机器上实际运行的速度有多快。我们可以执行负载测试,看看我们的机器/算法在折叠/变得不切实际之前可以获得多少实际输入。
答案 1 :(得分:23)
关于代码的推理,在最松散的单词中,意味着考虑你的代码以及真正所做的事情(不是你认为应该做的)。这意味着
等等。对我来说,推理部分在我调试或重构时扮演着最重要的角色。
使用你提到的一个例子:当我试图弄清楚函数有什么问题时,参考透明度对我有很大的帮助。引用透明性保证当我在使用函数时,给它不同的参数,我知道函数将在我的程序中以相同的方式作出反应。它不依赖于其参数以外的任何东西。这使得函数更容易推理 - 而不是命令式语言,其中函数可能依赖于一些可能在我的鼻子下改变的外部变量。
另一种看待它的方式(这在重构时更有用)是你越了解你的代码满足某些属性,就越容易理解。我知道,例如,
map f (map g xs) === map (f . g) xs
这是一个有用的属性,我可以直接在我重构时应用。我可以声明Haskell代码的这些属性的事实使得更容易推理。我可以尝试在Python程序中声明这个属性,但我会对它更加自信,因为如果我在选择f
和g
时运气不佳,结果可能会有很大变化
答案 2 :(得分:13)
非正式地意味着,“能够通过查看代码来告诉程序将会做什么。”由于副作用,转换,隐式转换,重载函数和运算符等原因,这在大多数语言中都会出乎意料地难以实现。也就是说,当你不能仅使用你的大脑推理代码时,你必须运行它才能看到什么它会为给定的输入做。
答案 3 :(得分:12)
通常当人们说“推理”时,他们的意思是“等式推理”,这意味着在不运行代码的情况下证明代码属性。
这些属性非常简单。例如,给定(.)
和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
证明这一点的。这意味着我知道无论如何这个属性总是如此,因此我不再需要在某种单元测试套件中测试这个属性,因为我知道它永远不会失败。
答案 4 :(得分:9)
“关于程序的推理”只是“分析程序以查看它的作用”。
这个想法是纯度简化了理解,既可以通过人工改变程序,也可以通过机器编译程序或分析程序来解决损坏的案例。
答案 5 :(得分:0)
正如@John Wiegley所说,理由是
通过查看代码
能够告诉程序将做什么
更重要的是要了解阻碍我们推理代码的因素。这些是副作用。
答案 6 :(得分:0)
关于这个问题的许多正确答案,已经提到了正确性的数学证明。但是我希望为不一定是数学专业的程序员提供一个实用的答案。
正如道格拉斯·克罗克福德(Douglas Crockford)所观察到的那样,形式化正确性的正式证明在当代编程实践中并不十分重要。[1]
在调试阶段,“关于代码的原因”一词对我来说首先具有实际意义。问题是: 出现问题时,确定问题原因的难易程度如何?
如果每个函数的行为仅取决于其输入,则预测函数体内可能发生的事情应该是微不足道的。意外错误意味着未处理某些输入案例。 (例如,一个常见的问题不是期望使用null
参数。)
另一方面,如果一个函数的结果取决于该函数不拥有或控制的外部变量的状态,那么当导致系统处于什么状态时,很难追踪导致该状态的原因。发生了错误。 (解决这些问题是系统执行许多日志记录的动机。)
这就是据说使用功能样式可以使您“合理”编写代码的原因。
并且,如果您知道在哪个函数中遇到了错误,那么您应该能够很容易地弄清楚什么地方出了问题以及如何解决。 (并且,如有必要,将调用堆栈展开到必须输入意外值的地方。)
“理性思考”也由行为驱动的开发范例证明:
给出:有限的可能的初始条件。 (例如,参数。)
何时:执行您定义的过程。
然后:可能的结果范围有限且已知。
简而言之,这是“合理的”代码。
(当然,这还取决于您的函数主体是否修改了外部变量,这就是函数程序员喜欢称之为“副作用”的东西。)
Edsger Dijkstra以反对goto
声明而著名。他认为,如果允许某个程序随意跳到其定义的任何行,那么您就无法希望预测其运行时行为。
函数式编程范例进一步扩展了这一点:它希望将程序逻辑外部的任何状态的影响限制在为实现其目的所必需的范围之内。
这样-在调试错误时-只需阅读代码就足以了解其原因。
[1]:克罗克福德,道格拉斯。 “测试如何进行”。 JavaScript的工作原理。 Virgule-Solidus LLC,2018年。EPUB。