为什么功能程序在编译成功与正确性之间有关联?


12

自从我第一次使用LINQ以来,我已经趋向于函数式编程已有4年了。最近,我写了一些纯函数式C#代码,并且第一手注意到我所读到的有关函数程序的内容-一旦编译,它们就趋于正确。

我试图指出为什么会这样,但是我没有成功。

一种猜测是,在应用OO主体时,您会拥有一个功能程序中不存在的“抽象层”,并且该抽象层使得在实现错误的情况下对象之间的契约可能正确。

有没有人考虑过这一点,并提出了函数式编程中编译成功与程序正确性之间相关性的根本抽象原因?


9
Lisp是一种功能语言,但没有编译时间检查可言。其他一些功能语言也是如此。您所谈论的语言的更准确的表征是:具有强大的正式(至少是Hindley-Milner)类型系统的语言。

1
@delnan我不会说Lisp是一种功能编程语言,尽管它可以用来编写功能编程代码。Clojure是Lisp的方言,是一种功能编程语言
sakisk 2014年

2
我同意@delnan。该语句与静态类型的函数编程语言(尤其是使用Hindley-Milner系统的Haskell)更为相关。我认为主要思想是,如果您正确选择类型,则可以提高程序正确性的信心。
sakisk 2014年

1
功能代码可以具有与典型的主流OOP代码一样多的抽象和间接寻址(如果不更多)。细节在于魔鬼-更少的副作用,没有null意味着更少的不可见状态要跟踪,更少的出错机会。请注意,您可以在主流命令式语言中应用这些相同的原则,这只是工作量更大,而且往往更冗长(例如,必须final对所有内容都打耳光)。
2014年

1
并不是完整的答案,但是键入程序程序形式验证的粗略形式。通常,面向对象程序具有复杂的类型系统或非常简单的类型系统,因为需要考虑替换,在大多数情况下,为方便起见它们不健全。像ML一样的系统OTOH可以完全使用CH,因此您可以仅按类型编码证明,并使用编译器作为证明检查器。
Maciej Piechotka 2014年

Answers:


12

我可以将这个答案写成可以证明很多事情的人,所以对我而言,正确性不仅在于有效,还在于有效且易于证明。

从很多意义上说,函数式编程比命令式编程更具限制性。毕竟,没有什么能阻止您从不更改C中的变量!实际上,FP语言中的大多数功能都可以直接用几个核心功能来谈论。一切都归结为lambda,函数应用程序和模式匹配!

但是,由于我们已经提前支付了吹笛者的费用,因此我们要处理的事情要少得多,而且事情出错的可能性也要少得多。如果您是1984年的粉丝,自由确实是奴隶制!通过为程序使用101种不同的巧妙技巧,我们必须对事物进行推理,好像这101种事物中的任何一种都可能发生!事实证明,这确实很难做到:)

如果从安全剪刀开始而不是剑开始,则跑步的危险性相对较小。

现在我们来看您的问题:所有这些如何适应“它的编译和运行!” 现象。我认为其中很大一部分原因与证明代码容易的原因相同!毕竟,当您编写软件时,您正在构造一些非正式的证明它是正确的。因此,您的自然波状证明涵盖了这些内容,并且编译器拥有正确性(类型检查)的概念。

当您添加功能以及它们之间的复杂交互时,类型系统未检查的内容会增加。但是,您构造非正式证明的能力似乎并没有提高!这意味着还有更多可以通过初始检查的内容,必须在以后进行检查。


1
我喜欢您的回答,但看不到它如何回答OP的问题
sakisk 2014年

1
@faif扩大了我的答案。TLDR:每个人都是数学家。
Daniel Gratzer

“通过为程序使用101种不同的巧妙技巧,我们必须对事情进行推理,好像这101种事情中的任何一种都可能发生!”:我读到某个地方,您需要成为天才才能进行突变编程,因为您必须保持如此你脑海中有很多信息。
乔治

12

函数式编程中编译成功与程序正确性之间相关性的根本抽象原因?

可变状态。

编译器会静态检查内容。它们确保您的程序格式正确,并且类型系统提供了一种机制,用于尝试确保在正确的位置允许使用正确的值。类型系统还试图确保在正确的位置允许使用正确的语义。

一旦您的程序引入状态,后一个约束就不再有用。您不仅需要担心正确位置的正确值,而且还需要考虑在程序的任意点更改该值的情况。您需要考虑与该状态一起更改的代码的语义。

如果您很好地进行了函数编程,则没有(或很少)可变状态。

但是,这里有一些因果关系的争论-如果没有状态的程序在编译后更频繁地工作,因为编译器可以捕获更多的错误,或者如果没有状态的程序在编译后更频繁地工作,因为这种编程风格产生的bug更少。

根据我的经验,可能两者兼而有之。


2
“在我的经历中很可能是两者的结合。”:我有相同的经历。当使用命令式语言(例如Pascal)时,静态类型也会在编译时捕获错误。在FP中,避免了可变性,并且我要补充一点,由于使用了更具声明性的编程风格,因此更易于推理代码。如果一种语言同时提供这两种语言,那么您将获得两种优势。
Giorgio 2014年

7

简而言之,这些限制意味着将事物放在一起的正确方法较少,而一流的功能使它更容易排除诸如循环结构之类的事物。以这个答案的循环为例:

for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
    String string = iterator.next();
    if (string.isEmpty()) {
        iterator.remove();
    }
}

这恰好是Java中在迭代过程中从集合中删除元素的一种安全的必要方法。有很多方法看起来很接近,但是都错了。人们不知道这种方法有时会采用复杂的方法来避免该问题,例如遍历一个副本。

使其通用并不困难,因此它不仅可以用于的集合Strings,而且还可以用于一流的函数,因此您无法替换谓词()中的条件if,因此该代码倾向于被复制和粘贴。并稍作修改。

结合一流的函数,这些函数使您能够将谓词作为参数传递,并且具有不变性的限制,如果不这样做,则非常烦人,并且您会想到简单的构建基块filter,例如此Scala代码做同样的事情:

list filter (!_.isEmpty)

现在考虑一下在编译时使用Scala的类型系统为您检查的内容,但是这些检查也由动态类型系统在您首次运行时进行:

  • list必须是支持该filter方法的某种类型,即集合。
  • 的元素list必须具有isEmpty返回布尔值的方法。
  • 输出将是具有相同类型元素的(可能)较小的集合。

一旦检查完这些内容,程序员还有什么其他方法可以解决?我不小心忘记了!,导致了非常明显的测试用例失败。那几乎是唯一可以犯的错误,而我之所以犯了这个错误,是因为我是直接从测试了逆条件的代码中翻译出来的。

一遍又一遍地重复这种模式。一流的功能使您可以将它们重构为具有精确语义的小型可重用实用程序,诸如不变性之类的限制使您有这样做的动力,并且对这些实用程序的参数进行类型检查几乎没有余地来固定它们。

当然,这全都取决于程序员知道简化功能filter已经存在,能够找到它或认识到创建自己的好处的程序员。尝试仅使用尾部递归在任何地方自己实现这一点,而您又回到了与命令式版本相同的复杂性中,更糟糕的是。仅仅因为您可以非常简单地编写它,并不意味着简单的版本显而易见。


“一旦检查完这些内容,程序员还有什么其他方法可以解决问题?”:这以某种方式证实了我的经验,即(1)静态类型+(2)函数样式少了一些解决问题的方法。结果,我倾向于更快地获得正确的程序,并且在使用FP时需要编写更少的单元测试。
Giorgio 2014年

2

我认为函数式编程编译与运行时正确性之间没有显着相关性。静态类型的编译和运行时正确性之间可能存在某种关联,因为如果您不进行强制转换,至少您可能拥有正确的类型。

正如您所描述的,可能以某种方式将成功的编译与运行时类型正确性相关联的编程语言方面是静态类型,即使如此,前提是您不使用只能在运行时声明的强制转换来弱化类型检查器(在具有强类型值或强类型(例如Java或.Net)或根本不强(在类型信息丢失或弱类型(例如C和C ++)的环境中)。

但是,函数式编程本身可能会以其他方式提供帮助,例如避免共享数据和可变状态。

这两个方面在正确性上可能都具有显着的相关性,但是您必须意识到,没有编译和运行时错误,从严格意义上讲,从更广泛的意义上讲,它并不能告诉您正确性,因为在程序中,它按照预期执行并且快速失败。无效输入或无法控制的运行时故障。为此,您需要业务规则,需求,用例,断言,单元测试,集成测试等。最后,至少在我看来,它们提供的功能比功能编程,静态类型输入或二者兼有。


这个。程序的正确性不能通过成功的编译来判断。如果编译器可以理解对程序规范做出贡献的每个人经常发生的冲突和不准确的要求,则可以认为成功的编译是正确的。但是那个神话般的编译器不需要程序员!尽管功能性命令式程序与命令性程序的编译性与正确性之间的总体相关性可能会略高,但这只是总体正确性判断的一小部分,我认为这基本上是无关紧要的
Jordan Rieger

2

经理说明:

一个功能程序就像一台大型机器,其中连接了所有东西,管道,电缆。[一辆车]

程序程序就像一栋建筑物,其中的房间里装有一台小型机器,将部分产品存储在垃圾箱中,并从其他地方获取部分产品。[一个工厂]

因此,当功能机器已经装配在一起时,它必然会产生一些东西。如果运行了一个复杂的程序,您可能会监督特定的效果,造成混乱,而不能保证其正常运行。即使您拥有所有已正确集成的清单,也有很多状态,可能的情况(部分产品散落,水桶满溢,丢失),因此很难提供保证。


但是,严重的是,过程代码没有像功能代码那样指定期望结果的语义。过程程序员可能更容易摆脱周围的代码和数据,并介绍了完成一件事情的几种方法(其中有些方法不完善)。通常,会创建无关的数据。当问题变得更加复杂时,函数式程序员可能需要更长的时间?

强类型的功能语言仍然可以进行更好的数据和流分析。使用过程语言时,通常必须在程序外部定义程序的目标,作为形式正确性分析。


1
更好的选择:函数式编程就像没有客户的服务台-一切都很棒(只要您不怀疑目的或效率)。
布伦丹2014年

@Brendan汽车与工厂并没有形成如此糟糕的比喻。它试图解释为什么功能语言中的(小规模)程序比“工厂”更可能工作并且出错率更低。但是为了说OOP的救助,工厂可以生产几件东西而且更大。您的比较是恰当的;经常听到FP可以并行化和优化的频率很高,但实际上(无双关)效果很慢。我仍然坚持FP。
Joop Eggen 2014年

对于en.wikipedia.org/wiki/Spherical_cow,大规模函数式编程非常有效 。
2014年

@Den我本人会担心在大型FP项目上工作不会出现可行性问题。甚至喜欢它。泛化有其局限性。但是,由于最后一个陈述也很笼统...(感谢球形母牛)
Joop Eggen 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.