Haskell函数是否可以通过正确性属性进行证明/模型检查/验证?


73

继续以下思想:是否存在可证明的现实世界语言?

我不认识你,但是我讨厌写我不能保证的代码。

在问完上述问题并得到了惊人的答复之后(谢谢!),我决定缩小对Haskell的可证明的,务实的方法的搜索范围。我选择Haskell是因为它实际上很有用(为此编写了许多 Web 框架,这似乎是一个不错的基准),而且我认为它在功能上非常严格,可能可以证明,或者至少允许测试不变式。

这就是我想要的(一直找不到)

我想要一个框架,可以查看以psudocode编写的Haskell函数:

add(a, b):
    return a + b

-并检查某些不变式是否保持在每个执行状态。我希望有一些正式的证明,但是我会选择模型检查器之类的东西。
在此示例中,不变的是给定值ab,返回值始终是a + b之和。

这是一个简单的示例,但我认为这样的框架不可能存在。可以测试的函数的复杂度肯定会有一个上限(一个函数的10个字符串输入肯定会花费很长时间!),但这会鼓励更仔细地设计函数,与使用其他形式函数没什么不同方法。想象一下使用Z或B,当您定义变量/集时,您一定要确保为变量提供尽可能小的范围。如果您的INT永远不会超过100,请确保将其初始化!我认为,像这样的技术以及适当的问题分解应该能够令人满意地检查像Haskell这样的纯功能语言。

我对形式方法或Haskell还不是很有经验。让我知道我的想法是否合理,或者您认为haskell不适合?如果您建议使用其他语言,请确保其通过“具有网络框架”测试,并阅读原始问题:-)


8
@Steven:真的吗?为什么会这样呢?如果一段代码已被证明具有某些属性,则无论与之交互的其他代码如何,它都将始终具有该属性,并且您可以在其他证明中依赖该属性。证明可以以没有测试的方式重复使用和组合,并且通过在类型系统中对证明进行编码,就不可能使它们与代码本身不同步。
CA McCann 2010年

3
@Steven:是的,我同意camccann,我不必测试房屋中的每种砖块组合。不错的山羊!
0atman 2010年

4
@Steven:他们不必这样做,这就是重点!如果一个函数对于所有可能的输入都是正确的,那么使用该函数的唯一义务就是提供适当类型的参数。完全不必担心集成点,这就是类型编码的证明是组成性的原因。知道保证有一段代码不会失败或不会产生运行时错误,这就是为什么静态类型很好的原因。
CA McCann 2010年

14
@Steven:就我个人而言,我宁愿花一些时间来编写不会中断的代码,而不是编写不可靠的代码,然后浪费时间编写琐碎的测试,并担心可能会出错的所有事情……但是对于我自己来说,我假设!
CA McCann 2010年

3
感谢您的意见,将其带到论坛:-)
0atman 2010年

Answers:


62

好吧,因为您正在沿着Haskell路线行驶,所以要开始做一些事情:

  • 您熟悉Curry-Howard的信件吗?有一些用于以此为基础的机器检查打样的系统,在许多方面,它们都是具有非常强大的类型系统的简单函数式编程语言。

  • 您是否熟悉抽象数学领域,这些领域为分析Haskell代码提供了有用的概念?各种形式的代数和一些类别理论都出现了。

  • 请记住,Haskell与所有图灵完备的语言一样,总是有可能终止。通常,要证明某事将永远是真实的,要比证明某事将是真实的或取决于一个非终止值要困难得多。

如果您正认真地寻求证明,而不仅仅是测试,那么这些就是要牢记的事情。基本规则是:使无效状态导致编译器错误。首先要防止对无效数据进行编码,然后让类型检查器为您完成繁琐的工作。

如果您想走得更远,如果记忆为我服务,证明助手Coq具有“提取到Haskell”功能,可让您证明关键函数的任意属性,然后将证明转换为Haskell代码。

对于直接在Haskell中做花式系统的工作,Oleg Kiselyov是大师。您可以在他的网站上找到一些示例,这些示例包括一些高级技巧,例如高级多态类型,用于对数组边界检查的静态证明进行编码

对于更轻量级的内容,您可以执行诸如使用类型级别证书将某条数据标记为已检查正确性的操作。您仍然需要自己进行正确性检查,但是其他代码至少可以依赖于知道实际上已经检查了某些数据。

从轻量级验证和奇特类型系统技巧的基础上,您可以采取的另一步骤是利用Haskell作为宿主语言来嵌入特定域的语言的效果很好;首先构造一个受严格限制的子语言(理想情况下,不是图灵完整的),您可以更轻松地证明该子语言的有用属性,然后使用该DSL中的程序在整个程序中提供关键的核心功能。例如,您可以证明两个参数的函数是关联的,以便证明使用该函数并行化项集合的合理化(因为函数应用程序的顺序无关紧要,仅参数的顺序无关紧要)。


哦,最后一件事。有关避免Haskell确实存在的陷阱的一些建议,这些陷阱可能会破坏原本可以安全地构建的代码:在这里,您发誓的敌人是一般递归IOmonad部分函数

  • 最后一个相对容易避免:不要编写它们,也不要使用它们。确保每个模式匹配都处理所有可能的情况,并且永远不要使用errorundefined。唯一棘手的部分是避免使用可能导致错误的标准库函数。有些显然是不安全的,例如fromJust :: Maybe a -> ahead :: [a] -> a但有些则可能更隐蔽。如果您发现自己编写的函数确实无法使用某些输入值做任何事情,那么您就可以通过输入类型对无效状态进行编码,并且需要首先对其进行修复。

  • 第二种方法很容易在表面上避免,方法是通过各种纯函数散布东西,然后再从IO表达式中使用它们。更好的是尽可能将整个程序移到纯代码中,以便可以使用除实际I / O之外的所有内容独立地对其进行评估。仅当您需要由外部输入驱动的递归时,这才变得棘手,这使我进入了最后一项:

  • 明智的话:有充分根据的递归生产性核心递归。始终确保递归函数要么从某个起点到某个已知的基本情况,要么按需生成一系列元素。在纯代码中,最简单的方法是通过递归折叠有限的数据结构(例如,而不是在将计数器增加到最大值之前直接调用自身的函数,创建一个保存计数器值范围并将其折叠的列表)或递归地生成惰性数据结构(例如,某个值的渐进逼近列表),同时请小心谨慎,切勿直接将二者混合使用(例如,不要只是“在满足某些条件的流中找到第一个元素”;它可能不会取而代之的是,从流中获取值直至某个最大深度,然后搜索有限列表,并适当地处理未找到的情况。

  • 将最后两项结合起来,对于您真正需要IO使用常规递归的部分,尝试将程序构建为增量组件,然后将所有尴尬的位浓缩为一个“驱动程序”功能。例如,您可以编写一个GUI事件循环,其中包含一个纯函数(例如)mainLoop :: UIState -> Events -> UIState,退出测试quitMessage :: Events -> Bool(一个),一个用于获取未决事件getEvents :: IO Events的函数和一个update函数updateUI :: UIState -> IO (),然后使用诸如的通用函数实际运行该事件runLoopIO :: (b -> a -> b) -> b -> IO a -> (b -> IO ()) -> IO ()。这使复杂的部分保持真正的纯净,使您可以使用事件脚本运行整个程序并检查结果UI状态,同时将笨拙的递归I / O部分隔离为一个易于理解且通常不可避免地正确的抽象函数。根据参数


谢谢,您认为可以直接对Haskell进行模型检查吗?
0atman 2010年

1
@Oatman:在一般情况下,尽可能对任何编程语言进行模型检查。由于纯度和类型安全性,许多可用于减少或简化要检查的状态空间的技术在Haskell中都是简单或自动的。但是,从根本上考虑国家转变可能会适得其反。
CA McCann 2010年

3
@Oatman:的确,Haskell的主要好处是无需外部工具就可以做很多事情。只要IO有可能就避免使用,通用递归和部分函数(对于大多数程序而言,“几乎在所有地方,都需要付出一些努力”)。
CA McCann 2010年

@camccann:我喜欢特定于域的想法。Haskell的方言设计为可证明的!这是我必须要尝试的东西,还是已经就您所知完成了类似的工作?
0atman 2010年

1
Coq确实具有“提取到Haskell”功能
Tom Crockett 2010年

21

可能最接近您要求的是Haskabelle,这是证明助手Isabelle附带的工具,可以将Haskell文件转换为Isabelle理论,并让您证明有关它们的内容。据我了解,该工具是在HOL-ML-Haskell项目中开发的,文档页面包含有关背后理论的一些信息。

我对这个项目不是很熟悉,对这个项目做得也不是很了解。但是我知道Brian Huffman一直在研究这些东西,查看他的论文和演讲,它们应该包含相关内容。


谢谢,我会检查一下,尽管我确实在寻找一种纯语言的模型检查方法。从更正式的语言到Haskell的翻译对于一般用途而言不是最佳的。
0atman 2010年

6
不,这是另一回事。Haskell被翻译成Isabelle。它可能仍然不是最佳选择,但它可能比Coq生成Haskell的方法更好。
svenningsson,2010年

哦,哦,哦!对不起,我读错了你的答案。嗯,这是不寻常的,更有用!
0atman 2010年

19

我不确定您的要求是否真的会让您感到高兴。:-)

通用语言的模型检查几乎是不可能的,因为模型必须是特定于领域的才能实用。由于哥德尔的不完备性定理,根本没有一种方法可以自动地以足够表达的逻辑来寻找证明

这意味着您必须自己编写证明,这引发了一个问题,即付出的时间是否值得。当然,这种努力会创造出非常有价值的东西,即确保您的程序正确无误。问题不是这是否是必须的,而是花费的时间是否过高。关于证明的事情是,尽管您可能对程序是正确的“直觉”的理解,但是通常很难将这种理解形式化为证明。直观理解的问题在于,它很容易发生意外错误(打字错误和其他愚蠢的错误)。这是编写正确程序的基本难题。

因此,有关程序正确性的研究就是使形式化证明和自动检查其正确性变得更加容易。编程是“易于形式化”的组成部分;以易于推理的方式编写程序非常重要。目前,我们具有以下范围:

  • 诸如C,C ++,Fortran,Python之类的命令式语言:在这里很难对任何东西进行形式化。单元测试和一般推理是至少获得一定保证的唯一方法。静态类型只能捕获琐碎的错误(比不捕获它们要好得多!)。

  • 像Haskell,ML这样的纯功能语言:富有表现力的类型系统可帮助捕获不重要的错误和错误。我要说,手动证明正确性对于大约200行左右的代码片段是可行的。(例如,我为操作包做了一个证明。)Quickcheck测试是形式化证明的廉价替代品。

  • 依赖类型的语言和Agda,Epigram,Coq等证明助手:得益于自动进行证明形式化和发现的帮助,证明整个程序正确无误。但是,负担仍然很高。

我认为,当前编写正确程序的最佳方法是纯函数式编程。如果生活取决于程序的正确性,则最好再提高一点,并使用校对助手。


要成为一个错误的学徒,您可能要说“没有效率的方法”,因为一个人可能会穷举枚举并连续地测试更长的证明,可能会受到越来越多的“燃料”的影响,从而避免
陷入困境

6
@sclv:这是解决暂停问题的一种方法:无限期地运行该程序,然后等待它是否暂停。您如何知道何时停止列举证明?简而言之,您可以通过这种方式寻找证明,但是您无法将“不存在证明”与“尚未发现(尚未)证明”区分开。
CA McCann

我选择了适当的“方法”定义,其中已经包含了效率问题。;-)
Heinrich Apfelmus

1
同样,Haskell的 SmallCheck库将详尽检查属性中的值范围Enum。对于某些属性的“一次性” 证明(对于每次构建均不重新计算)非常实用Enum
recursion.ninja 2014年

5

听起来像您想要ESC / Haskell:http : //research.microsoft.com/zh-cn/um/people/simonpj/papers/verify/index.htm

哦,Agda现在确实有一个Web框架(至少是概念验证):http : //www.reddit.com/r/haskell/comments/d8dck/lemmachine_a_web_framework_in_agda/


ESC / Haskell看起来不错,但根据我在本文中的略读,该实现是理论上的,并且有点局限。但是,这是一个很好的资源-谢谢!
0atman 2010年

何时可以在ghc中使用ESC / Haskell?
dan_waterworth 2011年

@dan_waterworth-不幸的是,我不知道这方面的任何计划。
sclv 2011年

不好意思,我真的可以在我正在做的项目中使用它。感谢回复。
dan_waterworth 2011年

4

您看过Quickcheck吗?它可能会提供您需要的一些东西。

http://www.haskell.org/haskellwiki/Introduction_to_QuickCheck


是的,我认为它看起来很棒,并且绝对是我进一步研究的基础。
0atman 2010年

2
以及smallcheck-smallcheck类似于quickcheck,但专注于针对“ small”案例进行全面测试,以实现“ small”的可配置概念。
mokus

1
我还记得在某个地方看到了一个程序包,该程序包会根据代数性质自动生成快速检查测试,例如声明两个函数的分布定律将产生f x (g y z)始终等于的检查g (f x y) (f x z)
CA McCann 2010年

还请检查Lazy Smallcheck,它利用了Haskell的惰性评估。
罗宾·格林

3

您看似简单的示例add(a,b)实际上很难验证-浮点数,上溢,下溢,中断,编译器验证,硬件验证等。

习惯是Haskell的简化方言,可用来证明程序的属性。

ume(Hume)是一种具有5个级别的语言,每个级别都有所限制,因此更易于验证:

全休H
  完全递归
休H
  原始递归函数
模板休ume
  预定义的高阶函数
  归纳数据结构
  归纳非递归一阶函数
FSM休ume
  非递归数据结构
休H
  没有功能
  非递归数据结构

当然,当今证明程序属性的最流行方法是单元测试,它提供了强大的定理,但是这些定理过于具体。“类型被认为有害”,皮尔斯,幻灯片66


1
算术可能是个问题。从理论上讲,如果您知道所有结果(包括中间结果)的大小,则可以做出保证。实际上,许多计算都不适合这样做。没有任意大小的整数,证明算术正确是很难的。对于浮点,请考虑docs.sun.com/source/806-3568/ncg_goldberg.html(每位计算机科学家应该了解的浮点知识),这是在许多非常聪明的人试图使浮点易于处理之后进行的。 。
David Thornley,2010年

3

看看芝诺(Zeno)。引用维基页面:

Zeno是针对Haskell程序属性的自动证明系统;由William Sonnex,Sophia Drossopoulou和Susan Eisenbach在伦敦帝国学院开发。它旨在解决对于任何输入值而言两个Haskell项之间相等的一般问题。

当今可用的许多程序验证工具都具有模型检查功能。能够非常快速地遍历非常大但有限的搜索空间。这些非常适合描述较多但没有递归数据类型的问题。另一方面,芝诺(Zeno)旨在通过归纳证明无限搜索空间中的属性,但仅限那些规格简单的属性。


2

正式证明Haskell程序的某些属性当然是可能的。在FP考试中,我必须这样做:给定两个表达式,证明它们表示相同的功能。由于Haskell是图灵完备的,因此一般不可能这样做,因此任何机械证明者都必须是证明助手(在用户指导下为半自动)或模型检查器。

已经朝这个方向进行了尝试,请参见例如P-logic:Haskell程序的属性验证使用Mizar证明功能程序的正确性。两者都是学术论文,其描述的方法多于实现。


我想补充一点,我至少在考试中感觉像是一个机械定理证明者;)
Fred Foo 2010年

嗯,在Google上“ mizar haskell”的第二个结果就是这个问题!!! 1:1(
0atman 2010年

这是通过Google和CiteseerX的快速运行。Mizar的这篇论文虽然发表在学术期刊上,但并不引人注目。
Fred Foo 2010年

@Oatman:没什么意思。堆栈溢出有疯狂的谷歌果汁。有关利基主题的问题很容易超越主要来源。
CA McCann 2010年

不错,但是关于mizar的信息在地面上非常薄,而不是我所希望的现有软件包。我将其添加到我的阅读列表中!
0atman 2010年


1

AProVE工具(至少)能够证明Haskell程序的终止,这是证明正确性的一部分。可以在本文中找到更多信息(简短版本)。

除此之外,您可能会对Dependent Types感兴趣。在这里,类型系统被扩展并用于使错误的程序成为不可能。


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.