joshdick
joshdick

Reputation: 1071

How to add docstring support to defn-like Clojure macro?

I wrote a macro to wrap a function definition in some helpful logging:

(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#)))

This works great for functions without docstrings:

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

(foo 3)
; INFO - foo input: 3
; INFO - foo output: 6
; 6

But if fails terribly for functions with docstrings:

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

How do I make my macro work for functions both with and without docstrings?

Upvotes: 3

Views: 875

Answers (2)

Brad Koch
Brad Koch

Reputation: 20267

I defined a generic function for identifying the parameters in this scenario.

(defn resolve-params
  "Takes parameters to a def* macro, allowing an optional docstring by sorting
   out which parameter is which.
   Returns the params, body, and docstring it found."
  [args]
  (if (string? (first args))
    [(second args) (drop 2 args) (first args)]
    [(first args) (rest args)]))

Then your macro definition becomes as simple as this:

(defmacro my-macro
  [fn-name & args]
  (let [[params body docstring] (resolve-params args)]
    ~your-macro-here))

Upvotes: 1

Konrad Garus
Konrad Garus

Reputation: 54015

One way to do it is to look at the arguments passed to defn-logged. If the first one after the name is a string use that as doc string, otherwise leave doc empty:

(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#))))

Test with doc string:

(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

Test without doc string:

(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

It still is not a complete equivalent of defn, at least because defn supports metadata passed in map, reader macro and string. But it works for doc strings.

Upvotes: 5

Related Questions