在组合大量功能时溢出堆栈

时间:2017-09-15 22:16:54

标签: f# stack-overflow tail-recursion continuations

这是展平二叉搜索树的一种方法。它的问题是当它构建的大函数最终应用于[]时,堆栈会溢出。我想知道是否有一种合理的方法来修复此代码片段而不完全改变它的工作方式。例如,构建一个构建函数树的自定义编写器,然后使用显式堆栈对它们进行评估(因为问题已经是扁平化树)会无济于事。

let flatten_k t =

    let rec f t (k:(list<'a>->list<'a>)->list<'a>) =
        match t with
        | Leaf -> 
            k (fun xs -> xs)

        | Bin (l,x,r) ->
            f l (fun fl -> f r (fun fr -> k (fl << (fun xs -> x::xs) << fr)))

    f t (fun g -> g [])

可能最好考虑一个简化的实例,尽管可能更难以令人信服地展示它的修复(因为它确实几乎没有,但至少它表明函数组合确实溢出了堆栈):

let test_composition () =
   let mutable f = id
   for i=0 to 1000000 do
       f <- id << f // >> works fine for me
   printf "Functions return %d" (f 123)

同样,这个问题不是关于如何压扁树。我可以很容易地做到如下,或者以任何数量纯粹的命令式方式。我想知道基于累积大函数的方法是否可以用于这个特定问题。非常感谢。

 let flatten t =

    let rec f t acc cont =
        match t with
        | Leaf ->
            cont acc

        | Bin (l, x, r) ->
            f r acc (fun rs -> f l (x::rs) cont)

    f t [] id

1 个答案:

答案 0 :(得分:5)

您的树展平函数不是尾递归。

函数的组合不是尾递归的。通过展开三重构图很容易看出:

original:              fl << cons << fr
unfold compositions:   fun a -> fl (cons (fr a))
unfold nested calls:   fun a ->
                          let x = fr a
                          let y = cons x
                          fl y

正如您所看到的,此函数首先调用fr,然后对其结果执行一些非常重要的操作。对fl的最后一次调用是尾递归的,但前两次不是。当frcons正在执行时,返回地址需要保留在堆栈中,没有办法解决这个问题。

这不是尾递归。尾递归的工作原理是将最后一次调用的结果传递给调用者。将此结果传递给另一个函数作为参数 - 这是完全不同的事情。

至于如何解决它 - 如果你坚持使用功能组合你就不能。如果你不坚持 - 那么你已经有了解决方案。

就你人为的例子而言 - 我认为它失败是因为你在FSI或类似的东西中运行它。我刚刚验证了它:

  • 如果你正常编译它,它可以正常工作。
  • 如果关闭优化,则会因堆栈溢出而失败。
  • 如果用一些非尾递归函数(例如id)替换fun x -> x+1,它也会失败。