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))))