【问题标题】:Accumulators, conj and recursion累加器、conj 和递归
【发布时间】:2012-05-26 19:56:11
【问题描述】:

我已经从 4clojure.com 解决了 45 个问题,并且在尝试使用递归和累加器解决一些问题的过程中发现了一个反复出现的问题。

我将尽我所能解释我正在做的事情,以最终得到令人毛骨悚然的解决方案,希望一些 Clojurer 能够“得到”我没有得到的东西。

例如,问题 34 要求编写一个函数(不使用 range),以两个整数作为参数并创建一个范围(不使用范围)。简单地说,你做 (... 1 7) 你得到 (1 2 3 4 5 6)。

现在这个问题不是要解决这个特定的问题。

如果我想要使用递归和累加器解决这个问题怎么办?

我的思考过程是这样的:

  • 我需要编写一个带有两个参数的函数,我以 (fn [x y] )

  • 开头
  • 我需要递归,我需要跟踪一个列表,我将使用一个累加器,所以我在第一个函数中编写了第二个函数,并带有一个额外的参数:

    (fn [x y]
    (((fn g [x y acc] ...) X 是的 '())

(显然我无法在 SO 上正确格式化 Clojure 代码!?)

这里我已经不确定我做的是否正确:第一个函数 必须 正好采用两个整数参数(不是我的调用),我不确定:如果我想使用累加器,我可以在不创建嵌套函数的情况下使用累加器吗?

然后我想conj,但是我做不到:

(conj 0 1)

所以我做了一些奇怪的事情来确保我首先得到了一个序列,我最终得到了这个:

(fn
   [x y]
   ((fn g [x y acc] (if (= x y) y (conj (conj acc (g (inc x) y acc)) x)))
    x
    y
    '()))

但是这会产生这个:

(1 (2 (3 4)))

而不是这个:

(1 2 3 4)

所以我最终做了一个额外的 flatten 并且它有效,但它非常丑陋。

我开始理解一些事情,在某些情况下,我什至开始以一种更流行的方式“思考”,但我在编写解决方案时遇到了问题。

例如在这里我决定:

  • 使用累加器
  • 通过递增 x 直到达到 y 来递归

但我最终得到了上面的怪物。

有很多种方法可以解决这个问题,但再次强调,这不是我想要的。

我所追求的是,​​在我决定 cons/conj、使用累加器并递归之后,我可以得到这个(不是我写的):

#(loop [i %1
        acc nil]
  (if (<= %2 i)
    (reverse acc)
    (recur (inc i) (cons i acc))))

而不是这个:

((fn
  f
  [x y]
  (flatten
   ((fn
     g
     [x y acc]
     (if (= x y) acc (conj (conj acc (g (inc x) y acc)) x)))
    x
    y
    '())))
 1
 4)

我认为能够解决一些问题是一个开始,但我对我倾向于产生的丑陋解决方案有点失望......

【问题讨论】:

  • 不要害怕扔掉不好的解决方案。如果您开始看到您的代码变得笨拙,请退后一步并重新考虑一下。当它感觉不对时,它可能不对。
  • @JeremyHeiler:好的,但“想法”并没有那么糟糕,“实现”/实际代码很糟糕。例如,使用累加器+递归的简短解决方案是由解决了 150 个 4clojure 问题的人编写的(其中一些问题确实不是微不足道的)。所以我的想法似乎还不错:但我(还)不能干净地实现我的想法。我认为拼图需要一些时间才能到位:-/
  • 确实如此。继续练习和玩代码!
  • 我对这些点赞和收藏感到有点惊讶:我可能不是唯一一个在尝试学习 Clojure(或任何 Lisp 方言)时面临同样问题的人。跨度>

标签: recursion clojure accumulator cons


【解决方案1】:

我认为这里有几件事要学习。

first,一种一般规则 - 递归函数通常具有自然顺序,而添加累加器则相反。你可以看到,因为当一个“正常”(没有累加器)递归函数运行时,它会做一些工作来计算一个值,然后递归生成列表的尾部,最后以一个空列表结束.相比之下,使用累加器时,您从空列表开始并在前面添加东西 - 它正在在另一个方向增长。

通常,当您添加累加器时,您会得到相反的顺序。

现在这通常无关紧要。例如,如果您生成的不是一个序列,而是一个重复应用 commutative 运算符(如加法或乘法)的值。那么无论哪种方式,您都会得到相同的答案。

但在你的情况下,这很重要。你将把列表倒过来:

(defn my-range-0 [lo hi] ; normal recursive solution
  (if (= lo hi)
    nil
    (cons lo (my-range-0 (inc lo) hi))))

(deftest test-my-range-1
  (is (= '(0 1 2) (my-range-0 0 3))))

(defn my-range-1 ; with an accumulator
  ([lo hi] (my-range-1 lo hi nil))
  ([lo hi acc]
    (if (= lo hi)
      acc
      (recur (inc lo) hi (cons lo acc)))))

(deftest test-my-range-1
  (is (= '(2 1 0) (my-range-1 0 3)))) ; oops!  backwards!

通常你能做的最好的解决方法就是在最后反转那个列表。

但这里有一个替代方案 - 我们实际上可以倒着做这项工作。而不是递增下限,你可以递减上限:

(defn my-range-2
  ([lo hi] (my-range-2 lo hi nil))
  ([lo hi acc]
    (if (= lo hi)
      acc
      (let [hi (dec hi)]
        (recur lo hi (cons hi acc))))))

(deftest test-my-range-2
  (is (= '(0 1 2) (my-range-2 0 3)))) ; back to the original order

[注意-下面还有另一种反转方式;我没有很好地构建我的论点]

second,正如您在my-range-1my-range-2 中看到的那样,编写带有累加器的函数的一种好方法是作为具有两组不同参数的函数。这为您提供了一个非常干净(恕我直言)的实现,而无需嵌套函数。


您还有一些关于序列的更一般性的问题,conj 等。这里的clojure有点乱,但也很有用。上面我一直在用基于缺点的列表给出一个非常传统的观点。但 clojure 鼓励您使用其他序列。与 cons 列表不同,向量向右而不是向左增长。所以 另一种 反转该结果的方法是使用向量:

(defn my-range-3 ; this looks like my-range-1
  ([lo hi] (my-range-3 lo hi []))
  ([lo hi acc]
    (if (= lo hi)
      acc
      (recur (inc lo) hi (conj acc lo)))))

(deftest test-my-range-3 ; except that it works right!
  (is (= [0 1 2] (my-range-3 0 3))))

这里conj 在右边添加。我没有在my-range-1中使用conj,所以这里重写一下更清楚:

(defn my-range-4 ; my-range-1 written using conj instead of cons
  ([lo hi] (my-range-4 lo hi nil))
  ([lo hi acc]
    (if (= lo hi)
      acc
      (recur (inc lo) hi (conj acc lo)))))

(deftest test-my-range-4
  (is (= '(2 1 0) (my-range-4 0 3))))

请注意,这段代码看起来非常类似于my-range-3,但结果是倒退的,因为我们从一个空列表开始,而不是一个空向量。在这两种情况下,conj 在“自然”位置添加新元素。对于右侧的向量,但对于左侧的列表。

我突然想到你可能并不真正理解什么是列表。基本上 cons 创建一个包含两个东西(它的参数)的盒子。第一个是内容,第二个是列表的其余部分。所以列表(1 2 3) 基本上是(cons 1 (cons 2 (cons 3 nil)))。相比之下,向量[1 2 3] 更像一个数组(尽管我认为它是使用树实现的)。

所以conj 有点令人困惑,因为它的工作方式取决于第一个参数。对于列表,它调用cons 并在左侧添加内容。但是对于向量,它将数组(类似事物)扩展到右侧。另请注意,conj 将现有序列作为第一个参数,将要添加的内容作为第二个参数,而 cons 则相反(首先要添加的内容)。


以上所有代码均可在https://github.com/andrewcooke/clojure-lab 获得


更新:我重写了测试,以便在代码生成列表的情况下,预期的结果是带引号的列表。 = 将比较列表和向量,如果内容相同,则返回 true,但明确显示您在每种情况下实际得到的内容会更清楚。请注意,前面带有''(0 1 2) 就像(list 0 1 2) - ' 停止评估列表(没有它,0 将被视为命令)。

【讨论】:

  • +1,这是一个很好的答案(其他答案也很好)。我需要一些时间来消化这一切,但有些东西已经为我“点击”了:)
【解决方案2】:

阅读完所有内容后,我仍然不确定您为什么需要蓄电池。

((fn r [a b]
    (if (<= a b) 
       (cons a (r (inc a) b)))) 
  2 4)
=> (2 3 4)

似乎是一个非常直观的递归解决方案。我在“真实”代码中唯一要改变的是使用惰性序列,这样你就不会在大范围内用完堆栈。

我是如何得到这个解决方案的:

当您考虑使用递归时,我发现尝试用您能想到的尽可能少的术语来陈述问题并尝试将尽可能多的“工作”交给递归本身会有所帮助。

特别是,如果您怀疑可以删除一个或多个参数/变量,这通常是可行的方法 - 至少如果您希望代码易于理解和调试;有时您最终会为了提高执行速度或减少内存使用而牺牲简单性。

在这种情况下,我开始写的时候的想法是:“函数的第一个参数也是范围的开始元素,最后一个参数是最后一个元素”。递归思维是你必须训练自己去做的事情,但一个相当明显的解决方案是:range [a, b] 是一个以元素 a 开头的序列 [a + 1, b]范围。所以范围确实可以递归地描述。我写的代码几乎就是这个想法的直接实现。

附录:

我发现在编写函数式代码时,最好避免使用累加器(和索引)。有些问题需要它们,但如果你能找到解决它们的方法,那么你通常会做得更好。

附录 2:

关于递归函数和列表/序列,在编写此类代码时最有用的思考方式是用“列表的第一项(头)”和“列表的其余部分(尾部)”。

【讨论】:

  • 好吧,这很好,但是您能否解释一下如何在您决定使用递归后最终会写出这些内容?请注意,另一个解决方案(使用累加器)是由解决 150 个 4clojure 问题的人编写的(其中一些更难的问题相当困难),所以使用累加器并不是那么牵强:)
【解决方案3】:

我无法添加您收到的已经很好的答案,但我会回答一般性的。在学习 Clojure 的过程中,您可能会发现使用 Clojure 内置函数可以解决许多但并非所有解决方案,例如 map 以及根据序列考虑问题。这并不意味着您不应该递归地解决问题,但是您会听到——而且我认为这是明智的建议——Clojure 递归用于解决您无法以其他方式解决的非常低级的问题。

我碰巧做了很多 .csv 文件处理,最近收到一条评论说 nth 创建依赖项。确实如此,并且使用地图可以让我通过名称而不是位置来比较元素。

我不会在已经投入生产的两个小型应用程序中丢弃使用 nth 和 clojure-csv 解析数据的代码。但下一次我会以更有序的方式思考问题。

很难从谈论向量和 nth、loop .. recur 等的书籍中学习,然后意识到学习 Clojure 会让你从那里成长。

我发现学习 Clojure 的好处之一是社区相互尊重且乐于助人。毕竟,他们正在帮助那些在 CDC Cyber​​ 上使用打孔卡学习的第一门语言是 Fortran IV,而他的第一门商业编程语言是 PL/I 的人。

【讨论】:

    【解决方案4】:

    如果我使用累加器解决了这个问题,我会这样做:

    user=> (defn my-range [lb up c]
             (if (= lb up)
               c
               (recur (inc lb) up (conj c lb))))
    #'user/my-range
    

    然后调用它

    #(my-range % %2 [])
    

    当然,我会使用letfn 或其他什么来解决没有defn 可用的问题。

    所以是的,您确实需要一个内部函数来使用累加器方法。

    我的思考过程是,一旦我完成,我想要返回的答案将在累加器中。 (这与您的解决方案形成鲜明对比,您在找到结束条件方面做了很多工作。)所以我寻找我的结束条件,如果我已经达到它,我会返回累加器。否则,我将下一个项目附加到累加器上,并在较小的情况下重复出现。所以只有两件事要弄清楚,结束条件是什么,以及我想在累加器中放入什么。

    使用向量有很大帮助,因为conj 会附加到它上面,而无需使用reverse

    I'm on 4clojure too,顺便说一句。我最近很忙,所以我落后了。

    【讨论】:

    • +1... 4clojure 上的“得分”不错 :) 我将发布一个关于“内部函数”与“不同参数集”的新问题。
    【解决方案5】:

    看起来您的问题更多是关于“如何学习”,而不是技术/代码问题。你最终会编写那种代码,因为无论你从什么方式或来源学习一般的编程或具体的 Clojure,都会在你的大脑中创建一条“神经高速公路”,让你以这种特定的方式思考解决方案,然后你最终会编写代码像这样。基本上,每当您遇到任何问题(在这种特殊情况下是递归和/或累积)时,您最终都会使用那个“神经高速公路”并且总是想出那种代码。

    摆脱这条“神经高速公路”的解决方案是暂时停止编写代码,远离键盘并开始阅读大量现有的 clojure 代码(从 4clojure 问题的现有解决方案到 github 上的开源项目)并深入思考它(甚至将一个函数读 2-3 次以真正让它在你的大脑中安定下来)。这样一来,您最终会破坏现有的“神经高速公路”(生成您现在编写的代码),并创建一条新的“神经高速公路”,以生成漂亮且惯用的 Clojure 代码。另外,尽量不要一看到问题就跳到输入代码,而是给自己一些时间来清楚而深入地思考问题和解决方案。

    【讨论】:

    • 这很有趣,但实际上我认为缺乏任何“Lisp 神经高速公路”让我编写这样的代码:我有点“看到”我想要解决方案的样子(我的想法是基本上与解决所有 150 个问题的用户相同)但是...我开始在 REPL 上玩并最终得到了错误的解决方案,但后来意识到我“知道”一个(超级糟糕的)来解决我的问题。例如,我最终得到 (1 (2 (3 4))) 并认为:“啊,我可以把它弄平”。当然是垃圾:(
    • Hmm.. 您的解决方案最终会创建另一个问题(嵌套列表),然后您解决这个新问题,并希望最终得到所需的解决方案,如果没有,那么您解决新的问题:) 和最后,您以某种方式设法解决了原始问题,但中间问题使您的代码成为那样。看起来您在解决问题时一次又一次地“查看整个问题”,而是继续使用蛮力
    • -1... 这正是我的问题。我特别声明我理解这个问题并且我理解解决它需要什么,但它是导致问题的实现。你一直在重写我写的内容,只是为了强调我“不明白”。抱歉,您的回答没有建设性。您显然不愿意在这里提供帮助:您唯一愿意做的就是重新声明:“我不明白”。你又在评论中这样做了:-1。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-06-30
    • 2021-07-06
    • 1970-01-01
    • 2013-12-25
    • 2022-10-06
    • 2012-12-06
    相关资源
    最近更新 更多