使用lazy-seq而不吹栈:是否可以将懒惰与尾递归结合起来?

时间:2012-07-15 20:35:45

标签: clojure stack-overflow lazy-evaluation

要了解Clojure,我正在4clojure解决问题。我正在问题164,我要在那里列举DFA接受的语言(部分)。一个有趣的条件是语言可能是无限的,因此解决方案必须是惰性的(在这种情况下,解决方案的测试用例(take 2000 ...

我有一个可以在我的机器上运行的解决方案,但是当我在网站上提交它时,它会炸掉堆栈(如果我增加可接受的字符串数量,从2000到20000确定,我也在本地吹掉堆栈,所以这是我的解决方案的缺陷)。

我的解决方案[1]是:

(fn [dfa]
  (let [start-state (dfa :start)
        accept-states (dfa :accepts)
        transitions (dfa :transitions)]
    (letfn [
      (accept-state? [state] (contains? accept-states state))

      (follow-transitions-from [state prefix]
          (lazy-seq (mapcat 
            (fn [pair] (enumerate-language (val pair) (str prefix (key pair))))
            (transitions state))))

      (enumerate-language [state prefix]
        (if (accept-state? state) 
          (cons prefix (follow-transitions-from state prefix))
          (follow-transitions-from state prefix)))
      ]   
      (enumerate-language start-state ""))
  )
)

它接受DFA

'{:states #{q0 q1 q2 q3}
              :alphabet #{a b c}
              :start q0
              :accepts #{q1 q2 q3}
              :transitions {q0 {a q1}
                            q1 {b q2}
                            q2 {c q3}}}

并返回DFA接受的语言(#{a ab abc})。但是,在确定前2000个接受的DFA字符串

(take 2000 (f '{:states #{q0 q1} 
                           :alphabet #{0 1}
                           :start q0
                           :accepts #{q0}
                           :transitions {q0 {0 q0, 1 q1} 
                                         q1 {0 q1, 1 q0}}}))
它吹了堆栈。显然我应该将解决方案重组为尾递归,但我不明白这是怎么回事。特别是,我看不出如何将懒惰与尾递归相结合(通过recurtrampoline)。 lazy-seq函数创建一个闭包,因此在recur中使用lazy-seq将使用闭包作为递归点。在lazy-seq内使用recur时,始终会评估lazy-seq,因为recur会发出需要评估其参数的函数调用。

使用trampoline时,我看不出如何迭代地构建一个可以延迟评估其元素的列表。正如我使用它并看到它使用的那样,trampoline只能在它最终完成时返回一个值(即其中一个trampolining函数不返回函数)。

其他解决方案被视为超出范围

我认为这个4Clojure问题的另一种解决方案超出了这个问题的范围。我目前正在使用iterate开发一个解决方案,其中每个步骤仅计算“下一步”(在当前statew的转换之后)接受的字符串,因此它根本不会递归。然后,您只跟踪当前状态和使您进入该状态的字符串(这些是下一个状态的前缀)。在这种情况下证明困难的是检测接受有限语言的DFA何时不再返回任何结果。我还没有为take-while周围的iterate设计一个合适的停止标准,但我很确定我会设法让这个解决方案起作用。对于这个问题,我对基本问题很感兴趣:懒惰和尾递归是否可以合并,还是根本不可能?

[1]请注意,网站存在一些限制,例如无法使用defdefn,这可能会解释我的代码的一些特殊性。

2 个答案:

答案 0 :(得分:6)

使用lazy-seq时,只需进行常规函数调用,而不是使用recur。懒惰避免了否则使用recur的递归堆栈消耗。

例如,repeat的简化版:

(defn repeat [x]
  (lazy-seq (cons x (repeat x))))

答案 1 :(得分:2)

问题是你正在构建一些看起来像:

的东西
(mapcat f (mapcat f (mapcat f ...)))

原则上这很好,但是这个列表最右边的元素很长时间都没有实现,当你意识到它们时,它们有大量的懒惰序列需要强迫以获得单个元素。

如果你不介意剧透,你可以在https://gist.github.com/3124087看到我的解决方案。我做的两件事与你不同,两者都很重要:

  1. 首先遍历树的广度。如果这是一个不接受的状态,你不希望在从q0到q0的循环中“卡住”。看起来这对于你失败的特定测试用例来说不是问题,因为过渡传递给你的顺序,但是在此之后的下一个测试用例确实具有这种特性。
  2. 使用doall强制我正懒惰地构建一个序列。因为我知道很多concat会构建一个非常大的堆栈,而且我也知道序列永远不会是无限的,我在构建它时强制整个事情,以防止导致堆栈的延迟序列的分层溢出。
  3. 编辑:通常,您不能将延迟序列与尾递归组合在一起。您可以使用一个同时使用它们的函数,可能在添加单个元素之前需要完成更多工作时重复执行,并在存在新元素时延迟重复,但大多数情况下它们具有相反的目标并尝试组合他们不经意地只会导致痛苦,而且没有特别的改善。