我是OCaml的新手,我从其他帖子中看到fold_left
中的List
是尾递归的,并且在较大的列表上效果更好,而fold_right
不是尾递归的。
我的问题是,fold_left
为什么只在较大的列表上才能更好地工作,如何实施使其在较小的列表上不能更好地工作。
答案 0 :(得分:1)
进行尾递归可以避免大量的内存分配。这种优化将与列表的长度成正比。
在小列表上会有所收获,但是直到您开始使用大列表时,它才可能引起注意。
根据经验,除非您使用的列表很小,并且应该使用fold_left
版本,否则fold_right
的含义应与您要编写的内容更加一致。
答案 1 :(得分:1)
fold_left
函数确实是尾递归的,但是,无论大小列表,它都可以正常工作。在小型列表上使用fold_right
代替fold_left
没有任何好处。 fold_left
功能总是比fold_right
快,并且您听到的谣言与fold_left
和fold_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))
和所有其他元素的总和。最终,它将推入所有值a
,a
,... b
,直到最终达到可以求和的x
和y
为止,并且然后展开通话堆栈。
与堆栈有关的问题是,它通常是有界的,这与堆内存不同,后者可能会不受限制地增长。这实际上并不是特定于数学或计算机语言设计的,这是现代操作系统运行程序的方式,它们为它们提供了固定大小的堆栈空间和未绑定的堆大小。并且一旦程序用完了堆栈大小,操作系统便终止了该程序,无法恢复。这非常糟糕,应尽可能避免。
因此,人们提出了一种更安全的z
实现,作为反向列表的左折。显然,这种折衷会导致执行速度变慢,因为我们必须从本质上创建输入列表的反向副本,并且只有在使用fold_right
函数遍历该列表之后,该反向副本才会实现。结果,我们将遍历该列表两次并产生垃圾,这将进一步降低代码的性能。因此,我们需要在标准库提供的快速但不安全的实现与其他库提供的安全可靠但缓慢的实现之间进行权衡。
总而言之,fold_left
总是比fold_left
快,并且总是尾递归。 fold_right
的标准OCaml实现不是尾递归的,这比其他一些库提供的fold_right
函数的尾递归实现要快。但是,这是有代价的,您不应将fold_right
应用于大型列表。通常,这意味着在OCaml中,您必须更喜欢fold_right
作为处理列表的主要工具。