避免 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 编写的,非常强大。您可以直接使用它,也可以用于比较目的。
我是 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 编写的,非常强大。您可以直接使用它,也可以用于比较目的。