List.fold_left实现与List.fold_right实现

时间:2019-04-08 16:19:54

标签: ocaml

我是OCaml的新手,我从其他帖子中看到fold_left中的List是尾递归的,并且在较大的列表上效果更好,而fold_right不是尾递归的。 我的问题是,fold_left为什么只在较大的列表上才能更好地工作,如何实施使其在较小的列表上不能更好地工作。

2 个答案:

答案 0 :(得分:1)

进行尾递归可以避免大量的内存分配。这种优化将与列表的长度成正比。

在小列表上会有所收获,但是直到您开始使用大列表时,它才可能引起注意。

根据经验,除非您使用的列表很小,并且应该使用fold_left版本,否则fold_right的含义应与您要编写的内容更加一致。

答案 1 :(得分:1)

fold_left函数确实是尾递归的,但是,无论大小列表,它都可以正常工作。在小型列表上使用fold_right代替fold_left没有任何好处。 fold_left功能总是比fold_right快,并且您听到的谣言与fold_leftfold_right无关,而是关于一条尾巴。 -fold_right的递归版本与fold_right的非尾递归版本。但首先让我强调一下right and left folds之间的区别。

左折显示元素列表

a b c d ... z

和一个函数f,并产生一个值

(f (f (f (f a b) c) d) ... z)

如果我们想象f是某种运算符(例如加法),并使用前缀表示法a + b而不是前缀表示法(add a b),则更容易理解。左折将序列减少为总和,如下所示:

((((a + b) + c) + d) + ... + z)

因此,我们可以看到左折将括号与左边联系起来。这是它与右折的唯一区别,后者实际上将括号关联到右,因此,如果我们采用相同的顺序并使用右折对其应用相同的功能,我们将进行以下计算

(a + (b + ... (x + (y + z))))

对于加法运算,左右折叠的结果将相同。但是,正确的折叠实施将效率较低。这样做的原因是,对于左折,我们一获得两个元素,例如a+b,就可以计算结果,对于右折,我们需要计算加{{ 1}}元素,然后添加第一个元素,例如n-1。因此,正确的折法必须将中间结果存储在某处。最简单的方法是使用堆栈,例如a + (b + ... + (y + z)),其中将a::rest -> a + (fold_right (+) rest 0))的值放到堆栈上,然后运行a计算,当准备好时,我们可以最终添加(fold_right (+) rest 0))和所有其他元素的总和。最终,它将推入所有值aa,... b,直到最终达到可以求和的xy为止,并且然后展开通话堆栈。

与堆栈有关的问题是,它通常是有界的,这与堆内存不同,后者可能会不受限制地增长。这实际上并不是特定于数学或计算机语言设计的,这是现代操作系统运行程序的方式,它们为它们提供了固定大小的堆栈空间和未绑定的堆大小。并且一旦程序用完了堆栈大小,操作系统便终止了该程序,无法恢复。这非常糟糕,应尽可能避免。

因此,人们提出了一种更安全的z实现,作为反向列表的左折。显然,这种折衷会导致执行速度变慢,因为我们必须从本质上创建输入列表的反向副本,并且只有在使用fold_right函数遍历该列表之后,该反向副本才会实现。结果,我们将遍历该列表两次并产生垃圾,这将进一步降低代码的性能。因此,我们需要在标准库提供的快速但不安全的实现与其他库提供的安全可靠但缓慢的实现之间进行权衡。

总而言之,fold_left总是比fold_left快,并且总是尾递归。 fold_right的标准OCaml实现不是尾递归的,这比其他一些库提供的fold_right函数的尾递归实现要快。但是,这是有代价的,您不应将fold_right应用于大型列表。通常,这意味着在OCaml中,您必须更喜欢fold_right作为处理列表的主要工具。