【问题标题】:Learning to read and write recursively学习递归读写
【发布时间】:2015-12-23 00:16:18
【问题描述】:

我是 Clojure 的新手,我以前在其他语言中看到过这种模式。我想我确实知道递归是如何工作的,但我不知道如何阅读和思考如何编写这样的代码。这是一个简单的例子:

(defn remove-dups [str]
  (when-let [[fst & rst] (seq str)]
    (if (= fst (first rst))
      (remove-dups rst)
      (cons fst (remove-dups rst)))))

我敢肯定,对于经验丰富的 Clojurist 来说,这看起来并不难。但是,我仍然不知道如何阅读这样的代码。也许有一种模式你必须学会​​理解?他们说 Lisp 和 Clojure 是“从内到外”读取的,但我找不到一种快速的方法来解析这样的代码。也许有一种方法可以通过打印步骤来“解开”递归模式?请有人教我以分而治之的方式思考。

【问题讨论】:

  • 旁白:这不是好的代码,因为它会消耗堆栈,如果需要太多递归步骤就会崩溃。如果loop / recurtrampoline 不能使用(并且循环/递归不能在这里使用,因为它不在尾部位置),一个不同的构造,即。 for,合适。
  • @CharlesDuffy 我听到了。对不起,我找不到更好的例子。如果我能写出更好的 Clojure 代码,我一开始就不会问这个问题。我认为虽然这段代码完美地说明了我的斗争,但斗争是真实的;[
  • 我听到了;不幸的是,我没有什么好的建议。我自己的方法包括长时间的重复曝光,直到点击成功,但这并不意味着我知道如何让其他人更轻松地完成这个过程。虽然,是的,一个人可以“解开”东西,但是虽然我可以用白板教它,但用文字并不容易。也许讨论在迭代算法和递归算法之间转换的过程会起作用,但简单的算法只适用于 TCO 的相同场景,而且这个例子不是其中之一。
  • 阅读this。更好的例子会有所帮助。
  • 另一边:通常你可以组合库函数,而不是直接cons将列表递归地组合在一起。这个例子变成了(->> str (partition-by identity) (map first))

标签: recursion clojure


【解决方案1】:

我怀疑这是许多想要接触 Clojure 的人都在问自己的问题,而且大多数人都不敢问,所以感谢您与我们联系。将您的大脑包裹在递归周围也是一项非常个人的工作。我是根据经验说的。回到大学时,我是介绍递归的课程的助教(五年),我花了五年时间试图找出“方法”来“向”人们“展示”如何像这样解开代码 sn-ps。

在那段时间里,我提出了这些似乎适用于所有情况的想法:

  • 第一次曝光时没有人可以阅读此代码
  • 一旦他们“明白”了,他们就无法以有用的方式向尚未“明白”的人解释它。
  • “获得”所需的时间完全是随机,并且与课程其他领域的成功没有任何关系(除非他们放弃了课程)

所以我的结论是学习阅读这段代码会以某种方式改变你的大脑,而这种改变需要随机的时间。每个人都以不同的方式学习它,每个人最终都在同一个地方。

以下是我为不同的人采取的一些方法:

  • 用调试器观察它(如果需要,可以持续几个小时):在 Clojure 中,这将是设置 Leiningen+emacs+clj-refactor 并向函数添加跟踪。然后运行它并反复执行它。
  • 在纸上画出堆栈(从页面底部开始)并运行许多示例一直。即使在他们声称理解它之后,仍然一路走来的人做得更好。
  • 有些人确实通过专注地盯着代码几个小时来学习这一点。作为一名教育工作者,除了不打断他们之外,我不知道如何提供帮助(老师经常打断学习)
  • 有些人走出教室,去喝酒,然后理解地回来。再说一次,不知道他们做了什么,尽管我怀疑他们比其他小组玩得更开心。

要有耐心,学会真正阅读这个例子会改善你作为程序员生活的方方面面,在我个人看来,这个例子完全没问题,非常适合这个目的。

【讨论】:

  • 通过The Little Schemer,一次一步,改变了我的大脑。我试过的其他方法都没有效果。
【解决方案2】:

我认为接触Mathematical induction 对理解递归有很大帮助。一旦您对此类数字证明感到满意,请尝试阅读一些基于树/图表的证明。数学归纳法通常从 n 到 n+1。递归通常是另一种方式——你有一个大小为 n 的问题,你将它减少到一个大小为 n-1 的问题。最终你会遇到一个大小为 1 或 0 的问题,这通常很容易解决。

通常用于序列的分而治之的策略是将它们分成头部(第一项)和(可能是空的)尾部(其余的项)。这是在上面的代码示例中使用[fst & rst] (seq str) 行完成的。这使用了一个称为解构的 Clojure 功能。相当于下面的

(let [fst (first str) rst (rest str)])

这是处理序列的大多数递归函数中的常见习语。划分序列后,您通常希望以某种方式将其重新组合在一起(可能更改一些元素,删除一些元素等)。你这样做的方式是cons使用cons 一次处理一个元素。

现在到remove-duplicates的细节:如果序列的头部等于尾部的第一个元素,那么我们有两个相等的连续元素,函数的语义要求我们删除其中一个.我们通过递归调用(remove-dups rst) 删除第一个(尾部)并继续处理序列的其余部分。如果它们不相同,那么我们需要在结果中保留头部,因此我们将其转换为 (remove-dups rst)(将在递归的下一步中计算)。

简化版如下。

(defn remove-dups [str] (if (empty? str) nil (let [fst (first str) rst (rest str)] (if (= fst (first rst)) (remove-dups rst) (cons fst (remove-dups rst))))))

进一步改进这一点的一种方法是在第 3 行返回一个空字符串 "" 而不是 nil。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-05-20
    • 1970-01-01
    • 2014-04-20
    • 2015-05-29
    • 1970-01-01
    • 2019-05-03
    • 2021-05-06
    相关资源
    最近更新 更多