避免 Clojure 中的递归堆栈溢出

Avoiding stack overflow from recursion in Clojure

我是 Clojure 的新手,无法弄清楚在某些情况下如何避免堆栈溢出。在尝试使用我发现名为 kern.

的解析器组合器库将解析项目移植到 Clojure 时,出现了这样一种情况。

Kern 为“many-till”解析器定义了一个递归实现:source

这适用于小输入:

(def input-text "The blue {cat} and the red {dog} became best friends with the white {wolf} END {not included}")

(def  between-brackets "parser that grabs all text between brackets"
  (between (sym* \{) (sym* \}) (<+> (many (none-of* "}")))))

(def parse-enclosed-words "parser that attempts to grab text between brackets, 
skips a character when it can't, and halts when it hits the string END"
  (many-till (<|> between-brackets (skip any-char)) (token* "END")))

(filter #(some? %) (value parse-enclosed-words input-text)) ;; => ("cat" "dog" "wolf")

不幸的是,随着输入字符串的增长,解析器遇到堆栈溢出:

(def file-input-text (slurp (io/resource "[input-text-20x-repeated.txt][2]") ))
(filter #(some? %) (value parse-enclosed-words file-input-text)) ;; => Unhandled java.lang.WhosebugError

根据我在网上阅读的内容,这可能是由于该函数使用了堆栈消耗递归。我试过使用“recur”关键字重写函数,但由于递归调用不在尾部位置,这似乎不起作用。

如何修改 many-till 以避免堆栈溢出?

我认为您无法使用库提供的抽象来做到这一点。这是 many-till 的一个非常自然的定义,并且在 Parsec 中工作得很好,这个库显然是 Kern 的灵感来源。但是 Clojure 没有 Haskell 的懒惰计算和自动蹦床,所以 many-till 构建的嵌套 lambda 不可避免地会消耗无限堆栈。您将需要一个更类似于 many 的实现,它通过函数手动构建解析器。我会在下面包含它的源代码,但我不拥有它,所以我认为我无权向 Stack Overflow 提供 CC BY-SA 4.0 许可,as posting it would do. Instead, here is a link to its source

已经有一个很好的答案讨论了这个特定库的递归细节。

关于使用 Clojure 解析数据这一更普遍的问题,您应该 查看高质量的解析器库 Instaparse。 它是用 Clojure 编写的,非常强大。您可以直接使用它,也可以用于比较目的。

另见 video from Clojure/West