对于在函数下关闭的集合,有一种明确的方法可以将二进制递归转换为尾递归,即为Fibonacci序列添加整数:
(使用Haskell)
fib :: Int -> Int
fib n = fib' 0 1 n
fib' :: Int -> Int -> Int
fib' x y n
| n < 1 = y
| otherwise = fib' y (x + y) (n - 1)
这是有效的,因为我们有我们想要的值y
和我们的操作x + y
,其中x + y
返回一个整数,就像y
一样。
但是,如果我想使用未在函数下关闭的集合,该怎么办?我想采用一个将列表拆分为两个列表的函数,然后对这两个列表执行相同的操作(例如递归创建二叉树),当另一个函数神奇地说它何时停止查看结果分割时我停止:
[1, 2, 3, 4, 5] -> [[1, 3, 4], [2, 5]] -> [[1, 3], [4], [2], [5]]
即,
splitList :: [Int] -> [[Int]]
splitList intList
| length intList < 2 = [intList]
| magicFunction x y > 0 = splitList x ++ splitList y
| otherwise = [intList]
where
x = some sublist of intList
y = the other sublist of intList
现在,如何将这个二进制递归转换为尾递归?先前的方法不会明确地工作,因为(Int + Int -> Int
与输入相同)但是(Split [Int] -/> [[Int]]
与输入不同)。因此,需要更改累加器(我假设)。
答案 0 :(得分:7)
有一个通用技巧可以使任何函数尾递归:在连续传递样式(CPS)中重写它。 CPS背后的基本思想是每个函数都需要一个额外的参数 - 一个在完成后调用的函数。然后,原始函数不是返回值,而是调用传入的函数。后一个函数称为“continuation”,因为它继续计算到下一步。
为了说明这个想法,我将以你的函数为例。请注意类型签名的更改以及代码的结构:
splitListCPS :: [Int] -> ([[Int]] -> r) -> r
splitListCPS intList cont
| length intList < 2 = cont [intList]
| magicFunction x y > 0 = splitListCPS x $ \ r₁ ->
splitListCPS y $ \ r₂ ->
cont $ r₁ ++ r₂
| otherwise = cont [intList]
然后你可以把它包装成一个看起来很正常的函数,如下所示:
splitList :: [Int] -> [[Int]]
splitList intList = splitListCPS intList (\ r -> r)
如果你遵循稍微复杂的逻辑,你会发现这两个函数是等价的。棘手的一点是递归情况。在那里,我们立即使用splitListCPS
致电x
。函数\ r₁ -> ...
告诉splitListCPS
完成后要做什么 - 在这种情况下,使用下一个参数(splitListCPS
)调用y
。最后,一旦我们得到两个结果,我们只是将结果组合并将其传递到原始延续(cont
)。所以最后,我们得到了我们原来的相同结果(即splitList x ++ splitList y
),但我们只是使用延续而不是返回它。
此外,如果您查看上面的代码,您会注意到所有递归调用都处于尾部位置。在每一步中,我们的最后一个操作始终是递归调用或使用延续。使用聪明的编译器,这种代码实际上可以非常有效。
从某种意义上说,这种技术实际上与你对fib
所做的相似;但是,我们不是维持累加器值,而是维护我们正在进行的计算的累加器。
答案 1 :(得分:2)
您通常不希望在Haskell中使用 tail-recursion 。你想要的是高效的核心运动(另见this),描述SICP中被称为迭代过程的内容。
您可以通过将初始输入括在列表中来修复函数中的类型不一致。在你的例子中
[1, 2, 3, 4, 5] -> [[1, 3, 4], [2, 5]] -> [[1, 3], [4], [2], [5]]
只有第一个箭头不一致,因此请将其更改为
[[1, 2, 3, 4, 5]] -> [[1, 3, 4], [2, 5]] -> [[1, 3], [4], [2], [5]]
说明了迭代应用concatMap splitList1
的过程,其中
splitList1 xs
| null $ drop 1 xs = [xs]
| magic a b > 0 = [a,b] -- (B)
| otherwise = [xs]
where (a,b) = splitSomeHow xs
如果在某次迭代中没有触发(B)
个案,您想要停止。
(编辑:删除中间版本)
但是,尽快生成已准备好的输出部分要好得多:
splitList :: [Int] -> [[Int]]
splitList xs = g [xs] -- explicate the stack
where
g [] = []
g (xs : t)
| null $ drop 1 xs = xs : g t
| magic a b > 0 = g (a : b : t)
| otherwise = xs : g t
where (a,b) = splitSomeHow xs
-- magic a b = 1
-- splitSomeHow = splitAt 2
不要忘记使用-O2
标志进行编译。