如何使用自然数折来定义斐波那契数列?

时间:2019-05-03 21:29:12

标签: haskell recursion fold recursion-schemes catamorphism

我目前正在学习结构递归/同构的意义上的折叠。我对自然数使用折线来实现幂和阶乘。请注意,我几乎不了解Haskell,因此代码可能很尴尬:

foldNat zero succ = go
  where
    go n = if (n <= 0) then zero else succ (go (n - 1))

pow n = foldNat 1 (n*)

fact n = foldNat 1 (n*) n

接下来,我想调整斐波那契数列:

fib n = go n (0,1)
  where
    go !n (!a, !b) | n==0      = a
                   | otherwise = go (n-1) (b, a+b)

使用fib,我有一对作为第二个参数,其字段在每次递归调用时都会交换。我被困在这一点上,因为我不了解转换过程的机制。

[编辑]

如注释中所述,我的fact函数是错误的。这是一个基于变态的新实现(希望如此):

paraNat zero succ = go 
  where 
    go n = if (n <= 0) then zero else succ (go (n - 1), n)

fact = paraNat 1 (\(r, n) -> n * r)

1 个答案:

答案 0 :(得分:2)

让类型指导您。这是您的foldNat,但带有类型签名:

import Numeric.Natural

foldNat :: b -> (b -> b) -> Natural -> b
foldNat zero succ = go
  where
    go n = if (n <= 0) then zero else succ (go (n - 1))

再看看您在go的实现中使用的fib助手,我们可以注意到递归的情况是接受并返回了(Natural, Natural)对。将其与foldNat的后继参数进行比较表明,我们希望b(Natural, Natural)。这很好地暗示了go的各个部分应该适合:

fibAux = foldNat (0, 1) (\(a, b) -> (b, a + b))

(我暂时忽略了严格性问题,但我会再讲一遍。)

这还不是fib,通过查看结果类型可以看出。不过,如Robin Zigmond所指出的那样,解决该问题没有问题。

fib :: Natural -> Natural
fib = fst . foldNat (0, 1) (\(a, b) -> (b, a + b))

这时,您可能需要向后工作,并用foldNat的定义来说明这与显式递归解决方案的对应关系。


虽然这是fib的一个非常好的实现,但它与您所写的却有一个主要区别:这是一个懒惰的正确对折(Haskell变形的规范也是如此),而您却是显然是严格的左折。 (是的,在这里使用严格的左折确实是有道理的:通常,如果您所做的工作看起来像算术,则理想情况下您希望使用严格的左折,而如果看起来像在构建数据结构,则您希望是惰性的)。好消息是,我们可以使用变形来定义几乎所有递归消耗值的东西……包括严格的左折!在这里,我将使用foldl-from-foldr技巧的改编版(有关列表,请参见this question的详细说明),该技巧依赖于以下函数:

lise :: (b -> b) -> ((b -> b) -> (b -> b))
lise suc = \g -> \n -> g (suc n)

这个想法是,我们利用函数组合(\n -> g (suc n)g . suc)的相反顺序来做事情-就像我们交换了succgo定义右侧的golise suc可以用作foldNat的后继参数。这意味着我们最终将得到一个b -> b函数,而不是b,但这不是问题,因为我们可以自己将其应用于零值。

由于我们想让 strict 左折,因此我们必须潜入($!)以确保对suc n进行急切的评估:

lise' :: (b -> b) -> ((b -> b) -> (b -> b))
lise' suc = \g -> \n -> g $! suc n

现在我们可以定义一个严格的左折(是foldNatfoldl'的{​​{1}}到Data.List的折叠):

foldr

还有最后一个重要的细节要处理:如果我们沿路径懒散地构建一对,则严格折叠就没什么用,因为对子组件将仍然是延迟构建的。我们可以使用foldNatL' :: b -> (b -> b) -> Natural -> b foldNatL' zero suc n = foldNat id (lise' suc) n zero ($!)来解决后继函数中的配对问题。但是,我相信使用严格的对类型更好,这样我们就不必担心:

(,)

data SP a b = SP !a !b deriving (Eq, Ord, Show) fstSP :: SP a b -> a fstSP (SP a _) = a sndSP :: SP a b -> b sndSP (SP _ b) = b 将字段标记为严格(请注意,您无需启用!即可使用它们)。

一切就绪,我们终于可以将BangPatterns作为严格的左折:

fib

P.S .:如合金所示,您的fib' :: Natural -> Natural fib' = fstSP . foldNatL' (SP 0 1) (\(SP a b) -> SP b (a + b)) 会计算 n ^ n 而不是 n!。最好将这个问题留给一个单独的问题。在任何情况下,其要点是阶乘更自然地表示为自然的同态,而不是简单的分解。 (有关更多信息,请参见例如Jared Tobin的Practical Recursion Schemes博客文章,更具体地讲关于同态的部分。)