Haskell以外的其他语言,例如Coq(禁止使用底部)或undefined
,或无限递归定义,例如
bot :: forall a. a
bot = bot
没有底层的好处很简单:所有程序都终止。编译器保证不存在无限循环,不存在无限递归。
还有一个不太明显的好处:语言的逻辑(由Curry-Howard correspondence给出)是一致的,不能证明是矛盾的。因此,相同的语言既可以编写程序,也可以编写程序正确的证明。但这可能不在这里。
防止无限递归的保护也很简单:强制每个递归定义都包含参数(此处bot
没有参数),并强制递归调用在这些参数之一上减少。在这里,递减是指代数数据类型,被视为构造函数和值的有限树。 Coq的编译器会检查递减的参数是否为ADT(在Haskell中为data
),并在参数的子树上(通常是通过case of
)而不是在其他地方的其他树上进行递归调用。
现在这种语言约束的代价是:我们失去了图灵完备性(因为我们无法解决halting problem)。这意味着存在终止功能,可以使用常规递归在Haskell中进行编码,而这些终止功能可能会被编译器拒绝。但是实际上,Coq库的数量表明,很少需要这些奇异的函数。有人甚至认识其中一个吗?
在某些情况下,无限循环是有意义的:
这些情况相当具体,可能会被新的语言原语处理。 Haskell引入了IO
来跟踪不安全的交互。为什么不声明函数签名中无限循环的可能性?还是将复杂的程序拆分为DSMS,它调用Haskell函数进行纯计算?
编辑
这是一个算法示例,如果我们转向总编程,则可能会阐明哪些更改。 Euclid算法计算2个数字的GCD,首先在纯递归Haskell中
euclid_gcd :: Int -> Int -> Int
euclid_gcd m n = if n <= 0 then m else euclid_gcd n (m `mod` n)
关于此函数,可以证明两点:它终止,并且确实计算m和n的GCD。在接受证明脚本的语言中,我们将向编译器提供(m mod n) < n
的证明,以便它得出递归在其第二个参数上递减的结果,因此终止。
在Haskell中,我怀疑是否可以这样做,因此我们可以尝试以结构递归形式重写此算法,以使编译器可以轻松地进行检查。这意味着必须在某些参数的前身上进行递归调用。这里m mod n
不会执行,因此似乎卡住了。但是像尾部递归一样,我们可以添加新的参数。如果我们找到了递归调用数量的界限,那就完成了。界限不必很精确,只需要高于递归调用的实际数目即可。这样的绑定参数在文献中通常称为variant
,我个人称为fuel
。当燃料用完时,我们强制递归以错误值停止。在这里,我们可以取两个数字中任意一个的后继者:
euclid_gcd_term :: Int -> Int -> Int
euclid_gcd_term m n = euclid_gcd_rec m n (n+1)
where
euclid_gcd_rec :: Int -> Int -> Int -> Int
euclid_gcd_rec m n fuel =
if fuel <= 0 then 0
else if n <= 0 then m else euclid_gcd_rec n (m `mod` n) (fuel-1)
这里,终止证明在某种程度上泄漏到了实现中,使其很难阅读。而且该实现对fuel参数进行了无用的计算,这可能会放慢一点,尽管在这种情况下,我希望Haskell的编译器可以使其忽略不计。 Coq具有提取机制,该机制可消除这种混合的证明和程序的证明部分,以生成OCaml或Haskell代码。
对于euclid_gcd
,我们将需要证明euclid_gcd_term
确实计算了n和m的GCD。这包括证明Euclid算法以少于n + 1的步长终止。
euclid_gcd_term
显然比euclid_gcd
的工作量更多,并且可以说是乐趣更少。另一方面,一旦养成了习惯,我就会发现在智力上知道我的算法的界限是有益的。当我找不到这样的界限时,通常意味着我不了解我的算法。这通常也意味着它们已被窃听。我们不能强迫所有开发人员对所有程序使用总编程,但是Haskell中的编译选项不是按需完成编译的好方法吗?
答案 0 :(得分:3)
我无法为您提供全面的答案,但是在过去的一年中,我花了一些时间在阿格达(Agda)工作,这是我所看到的一些总体缺陷。
基本上,在用Haskell编写程序时,我掌握了一些信息,但没有明确与编译器共享。如果该信息对于程序无误终止是必需的,那么Agda会强迫我将其明确显示。
考虑Haskell的Data.Map.!
运算符,可让您通过其键在地图中查找元素。如果传递的键不在地图中,它将引发异常。该运算符的Agda对应者不仅需要获取密钥,还需要获取密钥在地图中的证明。这有一些缺点:
m
包含键k
”,并证明有关该类型如何与插入和删除相互作用的引理。insert
和delete
的定义进行任何更改都可能使这些引理的证明无效。或者,我可以使用Maybe
或Either
来明确传递这些错误。通常这是正确的做法,但是当我预计会发生错误时,以及当我根本没有经历过显示错误是不可能的麻烦时,这并不清楚。这种方法也不适用于交互式调试器:我可以轻松地打破一个异常,但是在构造Nothing
时却不那么容易。
我一直在关注上面的错误,但是同样的事情也适用于非终止。
这并不是说全部语言都是无用的-正如您所说,它们有很多好处。到目前为止,我只是不会说所有应用程序的这些好处显然都超过了这些缺点。