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

对于函数下闭合的集合,有一种明确的方法可以将二进制递归转换为尾部递归,即斐波那契序列的整数加和:

(使用Haskell)

就是

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