Haskell 如何将这个二进制递归函数转换为尾部递归形式?
对于函数下闭合的集合,有一种明确的方法可以将二进制递归转换为尾部递归,即斐波那契序列的整数加和: (使用Haskell) 就是Haskell 如何将这个二进制递归函数转换为尾部递归形式?,haskell,functional-programming,binary-tree,tail-recursion,Haskell,Functional Programming,Binary Tree,Tail Recursion,对于函数下闭合的集合,有一种明确的方法可以将二进制递归转换为尾部递归,即斐波那契序列的整数加和: (使用Haskell) 就是 splitList :: [Int] -> [[Int]] splitList intList | length intList < 2 = [intList] | magicFunction x y > 0 = splitList x ++ splitList y | otherwise = [i
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
splitList::[Int]->[[Int]]
拆分列表intList
|长度intList<2=[intList]
|magicFunction x y>0=拆分列表x++拆分列表y
|否则=[intList]
哪里
x=intList的某个子列表
y=intList的另一个子列表
现在,如何将这个二进制递归转换为尾部递归?前面的方法不会显式工作,因为(
Int+Int->Int
与输入相同),但(Split[Int]-/>[[Int]]]
与输入不同)。因此,需要更改累加器(我假定)。使任何函数尾部递归都有一个通用技巧:用连续传递样式(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]
如果你遵循稍微复杂的逻辑,你会发现这两个函数是等价的。棘手的是递归情况。在那里,我们立即使用x
调用splitListCPS
。函数\r₁ -> ...
告诉splitListCPS
完成后要做什么——在本例中,使用下一个参数(y
)调用splitListCPS
)。最后,一旦我们得到了这两个结果,我们只需合并结果并将其传递到原始的延续(cont
)。最后,我们得到了与原来相同的结果(即splitList x++splitList y
),但是我们没有返回它,而是使用了continuation
此外,如果您仔细阅读上述代码,您将注意到所有递归调用都位于尾部位置。在每个步骤中,我们的最后一个操作总是递归调用或使用延续。有了一个聪明的编译器,这种代码实际上是相当有效的
从某种意义上说,这项技术实际上与您为fib
所做的类似;然而,我们不是维护累加器值,而是维护我们正在进行的计算的累加器 在Haskell中通常不需要尾部递归。您真正想要的是高效的协同过程(另请参见),描述所谓的迭代过程
通过将初始输入包含在列表中,可以修复函数中的类型不一致性。在你的例子中
[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
标志编译。根据我的经验,GHC中的多分支递归函数比它们的CPS或尾部递归(带有显式堆栈)对应函数更有效。例如,试着比较这三种风格的fib40
的计算时间。我想知道,它不是刚从煎锅里出来就进了火吗?我们得到了尾部递归,是的,但是我们没有将值保留在堆栈上,而是将它们作为lambda抽象保存在堆上,这在运行时可能会更加昂贵。我们的助手lambda函数是([[Int]->r),但我的拆分函数将[Int]拆分为一个列表,并拆分为[[Int],[Int]]。我怎么能有(\r->x),它的类型是[[Int]]->[Int]?@haskelline不幸的是,这个列表非常大,所以多分支递归会导致堆栈大小溢出,这就是为什么我需要将算法转换为尾部递归,然后我希望能够严格控制累加器的bang模式(我猜这就是这里的lambda函数)。@user1104160:这是一个输入错误。我的意思是让函数(\r->r)
或只是id
。此解决方案非常了解Haskell约定,我通常不使用这些约定,但应该使用这两种约定。我的问题是,尾部递归和corecursion显然非常相似,甚至在某种程度上是另一种的子集。在Haskell中,哪种做法更好?Haskell社区似乎更喜欢tail递归绕过任何堆栈大小溢出,而此解决方案似乎只是非常松散的递归(直到语句提示“while”类型的算法)。在这种情况下,这应该是标准做法(以最好地避免thunk累积)?或者
都没有什么特别之处;我只需要用“处理/不进一步处理”标记。我们也可以使用(0,)
或(1,)
,尽管使用或看起来更“干净”。至于尾部递归,这要看情况而定。如果你把某个不能按部分计算的东西作为一个整体来计算,那就更好了——在这种情况下,TR比“保护递归”/“更糟糕。”corecursion“。无论是什么术语,其本质是以最少的堆栈使用获得迭代的在线过程,从而尽快开始生成部分输出。这里的代码只是开始,前半步。@user1104160关键是拆分:您必须将整个输入拆分,还是像splitAt 10
这样的东西就足够了(这将消耗输入列表中的10个元素并返回(a,b)
,其中b
是尚未访问的输入部分。现在,您的魔术实际上
[[1, 2, 3, 4, 5]] -> [[1, 3, 4], [2, 5]] -> [[1, 3], [4], [2], [5]]
splitList1 xs
| null $ drop 1 xs = [xs]
| magic a b > 0 = [a,b] -- (B)
| otherwise = [xs]
where (a,b) = splitSomeHow xs
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