Clojure China

调整 Respo 当中 macro 时的一些感想

#1

近期因为对 Respo Effects 功能进行升级, 所以试着改了一些 macro 的实现. 留一些新的感想.

ClojureScript 在 shadow-cljs 当中提示不全

我的代码主要是运行在 ClojureScript 当中的, 用 shadow-cljs 编译,
但是报错的时候往往一大串搞得很头疼, 也看不清具体错在哪一行.
另外 ClojureScript 由于编译器的一些原因, 没法在 REPL 当中直接写 Macro.
至少要把 macro 写在一个 clj 文件或者某个 a.b.$macros 的命名空间, 才能生效,
对于调试来说相当麻烦, 所以我还是转用 Clojure 来调试宏.

所以现在我在项目当中会保留一个 clj 文件, 专门用来记录和提示宏.
clj 文件当中至少报错清楚很多, 比较适合调试定位.

在函数当中通过 macro 编入函数调用来插入调用栈

这个有点抽象, 大体上说我遇到的问题, 就是调用栈当中没有我需要的那一个函数调用,
就在代码这个代码的 children 当中, 有数据错误,
然而在其他地方递归调用时, 发现错误, 报错是定位在递归的代码当中的,
那么就很难找到原始的出现错误的位置了,

我想到的办法是借助 macro 多插入一行校验的代码, 也就是下面的 confirm-item.

(defn helper-create-el [el props children]
  `(respo.core/create-element ~(keyword el) ~props ~@(map confirm-item children)))

如果数据出错, 那么 confirm-item 会报错, 而这行调用就会出现在调用栈当中,
我的业务代码要写的简单一些, 通过 macro 来插入这样的代码, 算是方便很多.

不过从使用的效果看, macro 似乎有干扰到 Source Map 的效果,
显示的出错的位置, 大致在函数调用的结尾了, 并不是准确到元素上面.

defn 来强制生成函数名

代码逻辑内部用回调函数, 或者 let 临时创建的函数, 一般习惯用 fn.
但这个有个坏处是在 Chrome DevTools 当中得到的是一个 anonymous 的函数,
我的想到的办法是用 defn 创建一个函数名, 正好实现效果,

`(...
   (defn ~(symbol (str "call-" comp-name)) [~'%cursor] ~@body)
 ...)

ClojureScript 运行环境更为复杂

另外一个头疼的问题, 我其实没解决掉, 但是意识到之前我理解的有不对,
macro 是在编译过程当中运行的, 所以一般认为就是 JVM 环境.
但是实际遇到还会有比较麻烦的, 在 ClojureScript 当中, 部分代码是在 JavaScript 环境运行的.
这就导致说有的时候调用, 你在 JavaScript 环境当中读不到 JVM 环境当中运行的东西了.
`() 里面是 JavaScript 环境, 外面是 JVM 环境, 即便 namespace 一致, 也是不一定相互访问到.
这个引用当然还是最终在运行时能够引用的, 但是在 ClojureScript 就要特别小心区分开来.

比如下面的代码当中定义了一个 generate-component 函数, 用来分解 macro,
这段代码在 Clojure 中直接运行是没有问题的.

(defn generate-component [comp-name params & body]
  (println "inside" (pr-str comp-name) (pr-str params) (pr-str body))
  `(merge {}
    {:name ~(keyword comp-name),
     :render (fn [~@params]
               (defn ~(symbol (str "call-" comp-name)) [~'%cursor] ~@body))}))

(defmacro defcomp [comp-name params & body]
  (println "comp" (pr-str comp-name) (pr-str params) (pr-str body))
  (let [result (generate-component comp-name params body)]
   `(defmacro ~comp-name [~@params]
     (merge ~result {:args [~@params]}))))

但是到了 ClojureScript 当中, generate-component 就不存在了, 于是引用不到.
这样就需要尝试奇怪的写法, 而目前我并没有搞清楚这代码具体怎样处理.
有兴趣的话下载完整代码… https://gist.github.com/jiyinyiyong/366c7ae809977ef4551dd696b085d0c8

其他

难点很多. 用得多一些, 发现有强大的地方, 但是涉及到多个运行环境, 比函数头疼多了.

1赞
#2

With great power comes great undebuggablity :joy:

#3

我觉得写宏的话这样组织代码比较好。

xyz/core.clj

这个文件里面放宏的实现,宏的实现这样写

(defn def-xyz* [args]
  ...)

(defmacro ^{:arglists ...} def-xyz 
  [& args]
  (def-xyz* args))

这样写的好处就是,调试的时候只需要一个clj的REPL就可以了。专注于代码到代码的部分(def-xyz*).

xyz/core.cljs

这个文件放运行时的函数,也就是`()里面用到的函数。

同时要

(:refer-macros xyz.core)

这样使用的时候就不用再刻意:refer-macros了。