Clojure两种递归遍历形式

Spector的代码中出现以下两个函数

 
(defn ^:no-doc ic-prepare-path [locals-set path]
  (cond
    (vector? path)
    (mapv #(ic-prepare-path locals-set %) path)
 
    (symbol? path)
    (if (contains? locals-set path)
      `(com.rpl.specter.impl/->LocalSym ~path (quote ~path))
      ;; var-get doesn't work in cljs, so capture the val in the macro instead
      `(com.rpl.specter.impl/->VarUse ~path (var ~path) (quote ~path)))
 
 
    (i/fn-invocation? path)
    (let [[op & params] path]
      ;; need special case for 'fn since macroexpand does NOT
      ;; expand fn when run on cljs code, but it's also not considered a special symbol
      (if (or (= 'fn op) (special-symbol? op))
        `(com.rpl.specter.impl/->SpecialFormUse ~path (quote ~path))
        `(com.rpl.specter.impl/->FnInvocation
          ~(ic-prepare-path locals-set op)
          ~(mapv #(ic-prepare-path locals-set %) params)
          (quote ~path))))
 
 
    :else
    `(quote ~path)))
 
 
(defn ^:no-doc ic-possible-params [path]
  (do
    (mapcat
     (fn [e]
       (cond (or (set? e)
                 (map? e) ; in case inline maps are ever extended
                 (and (i/fn-invocation? e) (contains? #{'fn* 'fn} (first e))))
             [e]
 
             (i/fn-invocation? e)
             ;; the [e] here handles nav constructors
             (concat [e] (rest e) (ic-possible-params e))
 
             (vector? e)
             (ic-possible-params e)))
 
     path)))
 

如果抛开其中无关的具体细节, 而是关注他们的遍历和递归的方式, 会发现本质是一样的, 但是却用了不同的写法, 从理解代码的角度, 显然不应把他们看成两种不同的东西, 而且我们自己写的时候, 也应该保持某种连贯性, 如果意思和意图是相同的, 那么总应该用同一种形式来表现, 如同命名规范一样, 例如如果要一个变量表示total amount, 那么在一个地方写成totalAmount, 那么所有的地方都应该写成这样, 而不是用很多形式来表示同一个实质, 例如total_amount, m_TotalAmount之类的. 那么除了增加多余的噪音, 对代码质量没有好处.

下面针对第二个函数, 用两个版本来写

 
(defn fn-invocation? [path]
  (or (instance? clojure.lang.Cons path)
      (instance? clojure.lang.LazySeq path)
      (list? path))
)
 
(defn possible-params-simulate [path]
  (do
    (mapcat
      (fn [path]
        (cond      
          (vector? path)
          (possible-params-simulate path))
 
          (or (set? path)
              (map? path) ; in case inline maps are ever extended
              (and (fn-invocation? path) (contains? #{'fn* 'fn} (first path)))) 
          [path]
 
          (fn-invocation? path)
          (concat [path] (rest path) (possible-params-simulate path))
 
      )
      path
    ))
)
 
(defn possible-params-homo [path]
  (cond
    (vector? path)
    (mapcat #(possible-params-homo %) path)
 
    (or (set? path)
        (map? path) ; in case inline maps are ever extended
        (and (fn-invocation? path) (contains? #{'fn* 'fn} (first path)))) 
    [path]
 
    (fn-invocation? path)
    (concat [path] (rest path) (mapcat #(possible-params-homo %) path))
 
  )
)
 
(defmacro foo [& path]
  (let [
        _ (pprint (vec path))
        expanded (clojure.walk/macroexpand-all (vec path))
        ; expanded (riddley.walk/macroexpand-all (vec path))
        _ (println "after expand")
        _ (pprint expanded)
        prepared-path (simulate-prepare expanded)
        possible-params-simulate (vec (possible-params-simulate expanded))
        possible-params-homo (vec (possible-params-homo expanded))
 
       ]
     (pprint prepared-path)
     (println "----possible-params-simulate---")
     (pprint possible-params-simulate)
     (println "----possible-params-homo---")
     (pprint possible-params-homo)
   )
)
 
(defmacro bar [apath]
  `(foo ~apath)
  )
 
(bar [ALL (keypath "a") (keypath "b") ALL #(= 0 (mod % 3))])
(bar [ALL ALL #(= 0 (mod % 3))])
 

两者的区别在于, 是将mapcat function作为函数的主体, 然后在递归的时候, 增加一个mapcat的调用, 还是在函数的开头设置mapcat调用, 将mapcat function作为一个匿名函数. 区别在于递归发生的时候的调用形式, 如果是第二种, 那么在递归的地方直接使用函数名即可, 因为这里相当于隐含了一个mapcat的调用, 而第一种, mapcat function本身作为主体, 递归发生的时候就要手动的将函数绑定到mapcat上面, 所以第二种在转换为第一种的时候, 是将外层的mapcat移动到了所有的递归发生点, 其实在这里只展开一步的话和另外一种完全等价

 
(possible-params-simulate path)
=
(mapcat (fn [] ...) path)
=
(mapcat #(possible-params-homo %) path)
 

最后, 这个代码结构的实质是对一个数据结构做一个in place的transformation, 也就是遍历每一个节点, 并对节点做一定的transformation, 然后把结果写回到他原来的位置, 元素的内容会发生变化, 但是整个数据的结构不发生变化. 而这里数据的数据本身即是数据结构也可以是任意的合法的Clojure代码, 其实也就是一个AST. 因为是Homoiconic的.