为什么List.foldBack是通过可变数组实现的(而不是通过延续)?

时间:2012-10-01 19:53:16

标签: f# f#-powerpack

我一直在阅读F# core library sources(第2.0节),发现了一些相当有趣的东西:
List.foldBack是通过可变数组实现的,与List.fold不同,这非常简单。这是来源,您可能会发现它here

let foldArraySubRight (f:OptimizedClosures.FSharpFunc<'T,_,_>) (arr: 'T[]) start fin acc = 
    let mutable state = acc
    for i = fin downto start do
        state <- f.Invoke(arr.[i], state)
    state

// this version doesn't causes stack overflow - it uses a private stack 
let foldBack<'T,'State> f (list:'T list) (acc:'State) = 
    // skipped optimized implementations for known lists

    // It is faster to allocate and iterate an array than to create all those 
    // highly nested stacks.  It also means we won't get stack overflows here. 
    let arr = toArray list
    let arrn = arr.Length
    foldArraySubRight f arr 0 (arrn - 1) acc

不使用延续的原因是什么?
像下面的代码一样天真似乎只比高度优化的库方法慢2-3倍。看来它真的“更快地分配和迭代数组”似乎值得怀疑。此外,它是尾递归,所以这里没有StackOverflow 我错过了什么吗?

let foldBack2 predicate acc list =
    let rec loop list cont =
        match list with
        | [] -> cont acc
        | h::t -> loop t (fun racc -> cont (predicate h racc))
    loop list id

2 个答案:

答案 0 :(得分:10)

我可以想到一些不使用CPS的原因:

  1. 你为堆栈空间交换内存(所有这些延续都在堆上)
  2. CPS对大多数人来说都是不可理解的
  3. 正如您所发现的,它通常比其他选项慢(请参阅this question获取证据)
  4. 列表不能轻易地向后遍历,因为无法顺序访问数组(向前向后),向/从数组复制实际上非常有效。作为挑战,尝试为10,000 / 100,000 / 100,000,000元素编写更快的foldBack

答案 1 :(得分:5)

Make the common case fast

在实践中,您经常处理不会导致堆栈溢出的小列表。例如,OCaml中的F#List.foldBack的对应部分List.fold_right不是尾递归或使用CPS。

作为用户,我们并不关心内部实施是什么。我们喜欢快速和尾递归List.foldBack。例如,这个漂亮的split函数在F#中是尾递归的:

let split list = List.foldBack (fun x (l,r) -> x::r, l) list ([],[])