为什么Haskell会有底数(无限递归)?

时间:2018-08-11 14:00:26

标签: haskell recursion

Haskell以外的其他语言,例如Coq(禁止使用底部)或undefined,或无限递归定义,例如

bot :: forall a. a
bot = bot

没有底层的好处很简单:所有程序都终止。编译器保证不存在无限循环,不存在无限递归。

还有一个不太明显的好处:语言的逻辑(由Curry-Howard correspondence给出)是一致的,不能证明是矛盾的。因此,相同的语言既可以编写程序,也可以编写程序正确的证明。但这可能不在这里。

防止无限递归的保护也很简单:强制每个递归定义都包含参数(此处bot没有参数),并强制递归调用在这些参数之一上减少。在这里,递减是指代数数据类型,被视为构造函数和值的有限树。 Coq的编译器会检查递减的参数是否为ADT(在Haskell中为data),并在参数的子树上(通常是通过case of)而不是在其他地方的其他树上进行递归调用。

现在这种语言约束的代价是:我们失去了图灵完备性(因为我们无法解决halting problem)。这意味着存在终止功能,可以使用常规递归在Haskell中进行编码,而这些终止功能可能会被编译器拒绝。但是实际上,Coq库的数量表明,很少需要这些奇异的函数。有人甚至认识其中一个吗?

在某些情况下,无限循环是有意义的:

  • 交互式程序,其中用户通过单击或在键盘上键入来发出命令的程序,通常会永远运行。他们等待一个命令,对其进行处理,然后等待下一个命令。直到时间结束,或更严重的是直到用户发出quit命令为止。
  • 同样,处理无限的数据流而不是处理无限的用户命令流。例如对数据库的连续查询。

这些情况相当具体,可能会被新的语言原语处理。 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中的编译选项不是按需完成编译的好方法吗?

1 个答案:

答案 0 :(得分:3)

我无法为您提供全面的答案,但是在过去的一年中,我花了一些时间在阿格达(Agda)工作,这是我所看到的一些总体缺陷。

基本上,在用Haskell编写程序时,我掌握了一些信息,但没有明确与编译器共享。如果该信息对于程序无误终止是必需的,那么Agda会强迫我将其明确显示。

考虑Haskell的Data.Map.!运算符,可让您通过其键在地图中查找元素。如果传递的键不在地图中,它将引发异常。该运算符的Agda对应者不仅需要获取密钥,还需要获取密钥在地图中的证明。这有一些缺点:

  1. 有人必须提出一种类型,让我们表达“ map m包含键k”,并证明有关该类型如何与插入和删除相互作用的引理。
  2. insertdelete的定义进行任何更改都可能使这些引理的证明无效。
  3. 使用此地图时,我必须明确跟踪所有成员资格证明,将它们传递给周围并保持最新。这既是句法上的负担,又是精神上的负担。
  4. 如果我关心性能,则需要确保在运行时删除所有这些证明。

或者,我可以使用MaybeEither来明确传递这些错误。通常这是正确的做法,但是当我预计会发生错误时,以及当我根本没有经历过显示错误是不可能的麻烦时,这并不清楚。这种方法也不适用于交互式调试器:我可以轻松地打破一个异常,但是在构造Nothing时却不那么容易。

我一直在关注上面的错误,但是同样的事情也适用于非终止。

这并不是说全部语言都是无用的-正如您所说,它们有很多好处。到目前为止,我只是不会说所有应用程序的这些好处显然都超过了这些缺点。