如何向类似 defn 的 Clojure 宏添加文档字符串支持?

jos*_*ick 3 macros clojure

我写了一个宏来在一些有用的日志记录中包装一个函数定义:

(defmacro defn-logged
  "Wraps functions in logging of input and output"
  [fn-name args & body]
  `(defn ~fn-name ~args
     (log/info '~fn-name "input:" ~@args)
     (let [return# (do ~@body)]
       (log/info '~fn-name "output:" return#)
       return#)))
Run Code Online (Sandbox Code Playgroud)

这对于没有文档字符串的函数非常有用:

(defn-logged foo
  [x]
  (* 2 x))

(foo 3)
; INFO - foo input: 3
; INFO - foo output: 6
; 6
Run Code Online (Sandbox Code Playgroud)

但是如果对于带有 docstrings 的函数非常失败:

(defn-logged bar
  "bar doubles its input"
  [x]
  (* 2 x))
; IllegalArgumentException Parameter declaration clojure.tools.logging/info should be a vector
Run Code Online (Sandbox Code Playgroud)

如何让我的宏适用于有和没有文档字符串的函数?

Kon*_*rus 5

一种方法是查看传递给 的参数defn-logged。如果名称后的第一个是字符串,则将其用作 doc 字符串,否则将 doc 留空:

(defmacro defn-logged
  "Wraps functions in logging of input and output"
  [fn-name & stuff]
   (let [has-doc (string? (first stuff))
         doc-string (if has-doc (first stuff))
         [args & body] (if has-doc (rest stuff) stuff)]
     `(defn ~fn-name {:doc ~doc-string} ~args
        (println '~fn-name "input:" ~@args)
        (let [return# (do ~@body)]
          (println '~fn-name "output:" return#)
          return#))))
Run Code Online (Sandbox Code Playgroud)

用文档字符串测试:

(defn-logged my-plus "My plus documented" [x y] (+ x y))

(doc my-plus)
; -------------------------
; user/my-plus
; ([x y])
;   My plus documented
; nil

(my-plus 2 3)
; my-plus input: 2 3
; my-plus output: 5
; 5
Run Code Online (Sandbox Code Playgroud)

没有文档字符串的测试:

(defn-logged my-mult [x y] (* x y))

(doc my-mult)
; -------------------------
; user/my-mult
; ([x y])
;   nil
; nil

(my-plus 2 3)
; my-mult input: 2 3
; my-mult output: 6
; 6
Run Code Online (Sandbox Code Playgroud)

它仍然不是完全等效的defn,至少因为defn支持在 map、reader 宏和 string 中传递的元数据。但它适用于文档字符串。