【问题标题】:Folds versus recursion in ErlangErlang中的折叠与递归
【发布时间】:2012-04-13 16:57:54
【问题描述】:

根据Learn you some Erlang

几乎任何你能想到的将列表减少到 1 个元素的函数都可以表示为折叠。 [...] 这意味着 fold 是通用的,因为您可以在带有折叠的列表上实现几乎任何其他递归函数

在编写一个接受列表并将其减少为 1 个元素的函数时,我的第一个想法是使用递归。

哪些准则可以帮助我决定是使用递归还是折叠?

这是一种风格方面的考虑,还是还有其他因素(性能、可读性等)?

【问题讨论】:

    标签: recursion erlang fold


    【解决方案1】:

    我希望 fold 是递归完成的,因此您可能希望尝试使用 fold 实现一些不同的列表函数,例如 map 或 filter,看看它有多大用处。

    否则,如果您以递归方式执行此操作,则基本上您可能会重新实现折叠。

    学习使用语言附带的东西,这是我的想法。

    这个关于折叠和递归的讨论很有趣:

    Easy way to break foldl

    如果您查看本介绍中的第一段(您可能想阅读所有内容),他的陈述比我做得更好。

    http://www.cs.nott.ac.uk/~gmh/fold.pdf

    【讨论】:

      【解决方案2】:

      由于运行时中的优化实现,折叠通常更易读(因为每个人都知道他们在做什么)和更快(尤其是 foldl,它总是应该是尾递归的)。值得注意的是,它们只是更快的常数因素,而不是另一个顺序,因此如果您发现自己出于性能原因考虑其中一个而不是另一个,则通常是过早的优化。

      当您做一些花哨的事情时使用标准递归,例如一次处理多个元素,拆分为多个进程和类似的,并坚持使用高阶函数(折叠、映射、...)做你想做的事。

      【讨论】:

      • 我测试并发现平均而言,尾递归所花费的时间小于 foldl 对同一列表所花费的时间。有人可以解释为什么会这样吗?这是我的代码: Recursion = fun(List) -> R = fun F([]) -> []; F([I | R]) -> [I*3 | F(R)] 结束,T1 = os:timestamp(), L1 = R(List), T2 = os:timestamp(), {L1, timer:now_diff(T2, T1) / 1000} 结束。 Fold = fun(List) -> T1 = os:timestamp(), L1 = lists:foldl( fun(I, Acc) -> [I*3 | Acc] end, [], List ), T2 = os:timestamp (), {L1, timer:now_diff(T2, T1) / 1000} 结束。
      【解决方案3】:

      我个人更喜欢 Erlang 中的递归而不是折叠(与其他语言相反,例如 Haskell)。我没有看到 fold 比递归更具可读性。例如:

      fsum(L) -> lists:foldl(fun(X,S) -> S+X end, 0, L).
      

      fsum(L) ->
          F = fun(X,S) -> S+X end,
          lists:foldl(F, 0, L).
      

      rsum(L) -> rsum(L, 0).
      
      rsum([], S) -> S;
      rsum([H|T], S) -> rsum(T, H+S).
      

      似乎更多的代码,但它是非常简单和惯用的 Erlang。使用 fold 需要更少的代码,但随着有效负载的增加,差异会变得越来越小。想象一下,我们想要一个过滤器并将奇数映射到它们的平方。

      lcfoo(L) -> [ X*X || X<-L, X band 1 =:= 1].
      
      fmfoo(L) ->
        lists:map(fun(X) -> X*X end,
          lists:filter(fun(X) when X band 1 =:= 1 -> true; (_) -> false end, L)).
      
      ffoo(L) -> lists:foldr(
          fun(X, A) when X band 1 =:= 1 -> [X|A];
            (_, A) -> A end,
          [], L).
      
      rfoo([]) -> [];
      rfoo([H|T]) when H band 1 =:= 1 -> [H*H | rfoo(T)];
      rfoo([_|T]) -> rfoo(T).
      

      这里列表理解获胜,但递归函数排在第二位,折叠版本丑陋且可读性差。

      最后,折叠比递归版本更快是不正确的,尤其是在编译为本机 (HiPE) 代码时。

      编辑: 我根据要求在变量中添加了一个有趣的折叠版本:

      ffoo2(L) ->
          F = fun(X, A) when X band 1 =:= 1 -> [X|A];
                 (_, A) -> A
              end,
          lists:foldr(F, [], L).
      

      我看不出它比rfoo/1 更具可读性,我发现尤其是累加器操作比直接递归更复杂且不那么明显。代码更长。

      【讨论】:

      • 别忘了lists:zf/2。这是一个地图过滤器。使用 zf 你可以做lists:zf(fun(X) when X band 1 =:= 1 -&gt; {true, X * X}; (_) -&gt; false end, L)。不幸的是 zf 没有记录,所以没有正式存在......
      • 不同方法的不错比较!我认为您的第一个示例中的折叠更具可读性,但是对于如此简单的功能,差异当然很小。在您的第二个示例中,我实际上发现 fmfoo 比 rfoo 更具可读性,但是列表理解当然会为该问题赢得大量时间。所有这些都表明,每次都由开发人员为工作选择正确的工具:-)
      • 您发现递归更具可读性,因为您将 foldl 编写为 oneliner。试着把乐趣放在一个变量中,它看起来比递归更具可读性。
      【解决方案4】:

      旧线程,但我的经验是 fold 比递归函数慢。

      【讨论】:

        猜你喜欢
        • 2019-04-27
        • 1970-01-01
        • 1970-01-01
        • 2023-04-09
        • 2011-11-21
        • 2016-10-29
        • 2020-12-30
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多