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的.