Clojure 中的思考:避免面向简单字符串解析器的 OOP
Thinking in Clojure: Avoid OOP for simple string parser
我目前正在 Clojure 中实现一个小型解析器,它接受如下输入字符串:
aaa (bbb(ccc)ddd(eee)) fff (ggg) hhh
和returns没有括号内字符的字符串,即
(bbb(ccc)ddd(eee))(ggg)
我编写了以下函数:
(defn- parse-str [input]
(let [bracket (atom 0)
output (atom [])]
(doseq [ch (seq input)]
(case ch
\( (swap! bracket inc)
\) (swap! bracket dec)
nil)
(if (or (> @bracket 0) (= ch \)))
(swap! output conj ch)))
(apply str @output)))
对我有用:
(parse-str "aaa (bbb(ccc)ddd(eee)) fff (ggg) hhh")
"(bbb(ccc)ddd(eee))(ggg)"
但是我担心我的方法过于面向对象,因为它使用原子作为某种局部变量来保持解析器的当前状态。
是否可以从更函数式编程的角度编写相同的函数? (避开原子?)
也感谢任何改进我的代码的评论。
两种方式:可以使用显式递归或归约。
(defn parse-str [input]
(letfn [(parse [input bracket result]
(if (seq input)
(let [[ch & rest] input]
(case ch
\( (recur rest (inc bracket) (conj result ch))
\) (recur rest (dec bracket) (conj result ch))
(recur rest bracket (if (> bracket 0)
(conj result ch)
result))))
result))]
(clojure.string/join (parse input 0 []))))
(defn parse-str [input]
(clojure.string/join
(second (reduce (fn [acc ch]
(let [[bracket result] acc]
(case ch
\( [(inc bracket) (conj result ch)]
\) [(dec bracket) (conj result ch)]
[bracket (if (> bracket 0)
(conj result ch)
result)])))
[0 []]
input))))
在很多使用局部变量的情况下,您只需将任何变化的变量作为参数放入循环中,从而使用递归而不是变异。
(defn- parse-str [input]
;; Instead of using atoms to hold the state, use parameters in loop
(loop [output []
bracket 0
;; The [ch & tail] syntax is called destructuring,
;; it means let ch be the first element of (seq input),
;; and tail the rest of the elements
[ch & tail] (seq input)]
;; If there's no elements left, ch will be nil, which is logical false
(if ch
(let [bracket* (case ch
\( (inc bracket)
\) (dec bracket)
bracket)
output* (if (or (> bracket* 0) (= ch \)))
(conj output ch)
output)]
;; Recurse with the updated values
(recur output* bracket* tail))
;; If there's no characters left, apply str to the output
(apply str output))))
这是您函数的迭代版本;但它在功能上仍然是纯粹的。我发现像这样布置代码可以使其易于阅读。请记住,在使用递归时,始终首先检查您的终止条件。
(defn parse-str [s]
(loop [[x & xs] (seq s), acc [], depth 0]
(cond
(not x) (clojure.string/join acc)
(= x \() (recur xs (conj acc x) (inc depth))
(= x \)) (recur xs (conj acc x) (dec depth))
(<= depth 0) (recur xs acc depth)
:else (recur xs (conj acc x) depth))))
我目前正在 Clojure 中实现一个小型解析器,它接受如下输入字符串:
aaa (bbb(ccc)ddd(eee)) fff (ggg) hhh
和returns没有括号内字符的字符串,即
(bbb(ccc)ddd(eee))(ggg)
我编写了以下函数:
(defn- parse-str [input]
(let [bracket (atom 0)
output (atom [])]
(doseq [ch (seq input)]
(case ch
\( (swap! bracket inc)
\) (swap! bracket dec)
nil)
(if (or (> @bracket 0) (= ch \)))
(swap! output conj ch)))
(apply str @output)))
对我有用:
(parse-str "aaa (bbb(ccc)ddd(eee)) fff (ggg) hhh")
"(bbb(ccc)ddd(eee))(ggg)"
但是我担心我的方法过于面向对象,因为它使用原子作为某种局部变量来保持解析器的当前状态。
是否可以从更函数式编程的角度编写相同的函数? (避开原子?)
也感谢任何改进我的代码的评论。
两种方式:可以使用显式递归或归约。
(defn parse-str [input]
(letfn [(parse [input bracket result]
(if (seq input)
(let [[ch & rest] input]
(case ch
\( (recur rest (inc bracket) (conj result ch))
\) (recur rest (dec bracket) (conj result ch))
(recur rest bracket (if (> bracket 0)
(conj result ch)
result))))
result))]
(clojure.string/join (parse input 0 []))))
(defn parse-str [input]
(clojure.string/join
(second (reduce (fn [acc ch]
(let [[bracket result] acc]
(case ch
\( [(inc bracket) (conj result ch)]
\) [(dec bracket) (conj result ch)]
[bracket (if (> bracket 0)
(conj result ch)
result)])))
[0 []]
input))))
在很多使用局部变量的情况下,您只需将任何变化的变量作为参数放入循环中,从而使用递归而不是变异。
(defn- parse-str [input]
;; Instead of using atoms to hold the state, use parameters in loop
(loop [output []
bracket 0
;; The [ch & tail] syntax is called destructuring,
;; it means let ch be the first element of (seq input),
;; and tail the rest of the elements
[ch & tail] (seq input)]
;; If there's no elements left, ch will be nil, which is logical false
(if ch
(let [bracket* (case ch
\( (inc bracket)
\) (dec bracket)
bracket)
output* (if (or (> bracket* 0) (= ch \)))
(conj output ch)
output)]
;; Recurse with the updated values
(recur output* bracket* tail))
;; If there's no characters left, apply str to the output
(apply str output))))
这是您函数的迭代版本;但它在功能上仍然是纯粹的。我发现像这样布置代码可以使其易于阅读。请记住,在使用递归时,始终首先检查您的终止条件。
(defn parse-str [s]
(loop [[x & xs] (seq s), acc [], depth 0]
(cond
(not x) (clojure.string/join acc)
(= x \() (recur xs (conj acc x) (inc depth))
(= x \)) (recur xs (conj acc x) (dec depth))
(<= depth 0) (recur xs acc depth)
:else (recur xs (conj acc x) depth))))