【问题标题】:Clojure loop/recur pattern, is it bad to use?Clojure 循环/递归模式,用起来不好吗?
【发布时间】:2015-09-18 00:11:03
【问题描述】:

我正在学习 Clojure,我正在使用 4Clojure 作为资源。我可以解决网站上的许多“简单”问题,但对我来说,以函数式编程思维方式思考仍然不是自然而然的(我来自 Java)。因此,我在大多数 seq 构建实现中都使用循环/递归迭代模式,因为这就是我习惯的思考方式。

但是,当我查看更有经验的 Clojure 用户的答案时,他们以更实用的方式做事。比如in a problem about implementing the range function,我的回答如下:

(fn [start limit]
  (loop [x start y limit output '()]
    (if (< x y)
      (recur (inc x) y (conj output x))
      (reverse output))))

虽然这行得通,但其他用户做了这样的事情:

(fn [x y] (take (- y x) (iterate inc x)))

我的函数更冗长,我什至不知道“迭代”函数存在。但从效率的角度来看,我的回答是否更糟?循环/重复使用是否比替代方案更糟糕?我担心这种事情将来会在我身上发生很多,因为还有很多我不知道的函数,比如迭代。

【问题讨论】:

  • 如果您切换到使用向量而不是列表,则无需反转最终输出。 conj-ing 到一个向量上添加到末尾。试试(conj '(0) 1)(conj [0] 1) 看看有什么不同。所以改成output [],把反面放在最后。

标签: clojure


【解决方案1】:

第二个变体返回一个惰性序列,这确实可能更有效,尤其是在范围很大的情况下。

另一件事是第二种解决方案更好地传达了这个想法。换句话说,它描述的是意图而不是实现。与您必须通读循环体并在头脑中构建控制流模型的代码相比,理解它所需的时间更少。

关于新功能的发现:是的,您可能事先不知道某些功能已经定义。例如,在 Haskell 中更容易,您可以在其中通过类型签名搜索函数,但是通过一些经验,您将学会识别这样的函数式编程模式。您将编写类似于第二个变体的代码,然后在标准库中查找类似于 takeiterate 的代码。

【讨论】:

    【解决方案2】:

    Clojure Cheetsheet website 加入书签,并始终打开浏览器选项卡。

    研究所有函数,尤其是阅读它们链接到的示例(http://clojuredocs.org 网站)。

    网站http://clojure-doc.org 也很有用(是的,两个名字几乎相同但不完全一样)

    【讨论】:

      【解决方案3】:

      问题不应该是关于性能(这取决于!),而是关于通信:当使用loop/recur 或普通递归或lazy-seq 或有时甚至reduce 时,您会使您的代码更难理解:因为读者已经在了解您正在计算的内容之前了解您是如何执行迭代的。

      【讨论】:

        【解决方案4】:

        loop/recur 是真正的 Clojure,而且是惯用的。它的存在是有原因的。而且通常没有更好的方法。但是很多人发现,一旦习惯了,用iterate这样的积木来构建很多功能是非常方便的。 Clojure 有一个非常好的集合。我开始使用真正的递归算法从头开始编写东西,然后循环/递归。就个人而言,我不会声称使用功能构建块函数会更好,但我已经开始喜欢使用它们。这是 Clojure 的一大优点。

        (是的,许多构建块函数都是惰性的,例如formap,它们更通用。惰性可能是好的,但我并不相信它。有时它是更有效率。有时不是。有时它很漂亮。有时它是一个痛苦的人。有时就是这样。)

        【讨论】:

          【解决方案5】:

          循环和递归都不错——事实上,如果你查看许多内置函数的源代码,你会发现它们就是这样做的——提供的函数通常是对常见模式的抽象,可以让你的代码更容易理解。许多人刚开始做事时,你是如何做事的。你如何接近这个对我来说似乎是正确的。您不仅仅是在编写解决方案并继续前进。您正在编写解决方案,然后查看其他人如何解决相同的问题并进行比较。这是改善的正确道路。强烈建议当你找到一个看起来更优雅/高效/清晰的替代解决方案时,分析它,查看它使用的内置函数的源代码,事情就会慢慢融合在一起。

          【讨论】:

            【解决方案6】:
            • loop ... recur 是递归尾调用的优化,应该 始终在适用的地方使用。
            • range 是懒惰的,所以你的版本应该努力做到这一点。 loop ... recur 不能这样做。
            • 所有可能是惰性的序列函数(iterate
              filtermaptake-while ...)都是如此。如您所知,您可以使用其中一些 建立一个懒惰的range。正如@cgrand 解释的那样,这是首选方法。

            如果你愿意,你可以从头开始构建一个懒惰的range

            (defn range [x y]
              (lazy-seq
               (when (< x y)
                 (cons x (range (inc x) y)))))
            

            【讨论】:

              【解决方案7】:

              几天来我一直在想同样的事情,但确实很多时候我没有看到比循环重复更好的选择。 有些工作没有完全“减少”或“映射”。当您在每次迭代时都发生变异的缓冲区上更新数据库时就是这种情况。

              循环递归在需要“非线性精确工作”的情况下非常方便。它看起来更有必要,但如果我没记错的话,Clojure 是用实用主义设计的。还是买吧,务实就是选择更高效的东西。

              这就是为什么在复杂的程序中,我同时使用 Clojure 和 java 代码。有时 java 对于“低级”或迭代工作(例如获取特定值等)更清楚,而我看到 Clojure 函数对大数据处理更有用(没有这么多细节:全局过滤器等)。

              有人说我们必须尽可能多地使用 Clojure,但我看不出有什么理由不使用 Java。我的编程不多,但 Clojure/Java 是我见过的最好的互操作,非常互补的方法。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2011-02-08
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多