在看Spector的代码, 碰到类似下面的代码
used-locals-cell (i/mutable-cell []) _ (cljwalk/postwalk (fn [e] (if (local-syms e) (i/update-cell! used-locals-cell #(conj % e)) e)) path) used-locals (i/get-cell used-locals-cell)
这段代码也许只是完成一个简单的操作, 但是写法让我感觉不那么elegant.
这里面几乎违反了几个FP的最重要原则: 一个是mutation, 一个是副作用. 问题的本质是遍历某种数据结构, 并在每一个节点上收集必要的数据, 收集的点是一个函数, 可以称之为callback, 或者visitor, 在这个函数里面要把数据集中到某个store里面.
思考了一下之后发现, 不用mutation是无法做到的. 这个例子中为了实现mutable, 直接动用了底层的JAVA, mutable-cell是一个Java 类, 其唯一的作用就是实现一个mutable的store. 像Clojure这样提供了优秀的immutability工具, 但是出于实际的考虑, 我们有时候为了mutation不得不煞费苦心. 不过只要控制得当, 谨慎使用, 还是可以接受的. 原则就是不要mutate任何东西, 除非不得不如此.
public class MutableCell { private Object o; public MutableCell(Object o) { this.o = o; } public Object get() { return o; } public void set(Object o) { this.o = o; } }
既然引入mutation不可避免, 那么我们也可以把事情做得得当一些. 其实Clojure本身提供很多工具来实现mutation. 例如atom, ref, 还有transient. transient在这个例子中并不适用, transient的主要作用是优化性能, 而这里是要产生副作用, 副作用本身就是对一个data store进行mutate.
如果不动用额外的Java类, 也可以实现同样的效果, 另外一点就是整个过程最好封装到一个函数当中, 涉及到mutation的部分最好应该对外不可见, 就像transient一样. 不应该把mutate的部分和其他代码混在一起. 另外在Clojure中写很长很大的函数几乎总是不太好的做法. 应该尽量写小而简洁的函数, 并始终注意尽量避免mutation, 除非万不得已.
下面的代码使用内置的mutation工具atom, 并且将mutate的部分封装在函数中, 接受的和返回的都是persistent data structure.
(defn extract-used-locals [local-syms path] (let [ used-locals-accumulator (atom []) ] (clojure.walk/postwalk (fn [ele] (if (local-syms ele) (swap! used-locals-accumulator (fn [a] (conj a ele))) ele ) ) path ) @used-locals-accumulator ) ) (defmacro foo [& path] (pprint (extract-used-locals #{(symbol "ALL")} path)) ) (foo [ALL ALL #(= 0 (mod % 3))])