Andrew
Andrew

Reputation: 1660

How can I iterate over a list with a macro?

I am trying to print the documentation for all functions in a given namespace by invoking the following expression in a REPL:

(doseq
  [f (dir-fn 'clojure.repl)]
  (doc f))

However the invocation of this expression returns nil without printing the documentation to the REPL. I know this might have to do with doc being a macro, but I'm a Clojure novice and am not entirely sure how to understand the problem.

  1. Why does this expression return nil without printing the documentation?
  2. How can this expression be modified so that it prints the documentation for each function in a given namespace?

Thanks!

Update: Combined both provided answers:

(defn ns-docs [ns']
  (doseq [[symbol var] (ns-interns ns')]
  (newline)
  (println symbol)
  (print "  ")
  (println (:doc (meta var)))))
(ns-docs 'clojure.repl)

Upvotes: 1

Views: 461

Answers (3)

l0st3d
l0st3d

Reputation: 2968

While this probably won't help you with answering your question, the problem of evaluating macro's comes up a lot when you are learning Clojure.

Macros are responsible for the evaluation of their arguments. In this case clojure.repl/doc will ignore the current lexical context and assume that the symbol f that you're giving it is the name of a function you want to see the documentation for. It does this because it's intended to be used at the REPL, and is assuming you wouldn't want to type quotes all the time.

As f doesn't exist, it prints nothing. Then doseq returns nil, since it exists to do something for side effects only - hence starting in do. In order to pass an argument to a macro that refuses to respect the lexical context like this, you need to write the code for each element in the list.

You can do this by hand, or by constructing the code as data, and passing it to eval to execute. You can do this in an imperative style, using doseq:

(doseq [f (ns-interns 'clojure.repl)]
  (eval `(doc ~(symbol "clojure.repl" (str (first f))))))

or in a slightly more Clojurey way (which will allow you to see the code that it would execute by removing eval from the end and running it at the REPL):

(->> (ns-interns 'clojure.repl)
     (map #(list 'clojure.repl/doc (symbol "clojure.repl" (str (first %)))))
     (cons `do)
     eval)

In both of these we use quote and syntax-quote to construct some code from the list of symbols reflected from the namespace, and pass it to eval to actually execute it. This page on Clojure's weird characters should point you in the right direction for understanding what's going on here.

This an example of why you shouldn't write macro's, unless you've got no other options. Macro's do not compose, and are often difficult to work with. For a more in depth discussion, Fogus's talk and Christophe Grand's talk are both good talks.

Upvotes: 2

Taylor Wood
Taylor Wood

Reputation: 16194

Why does this expression return nil without printing the documentation?

Because the doc macro is receiving the symbol f from your loop, instead of a function symbol directly.

How can this expression be modified so that it prints the documentation for each function in a given namespace?

(defn ns-docs [ns']
  (let [metas (->> (ns-interns ns') (vals) (map meta) (sort-by :name))]
    (for [m metas :when (:doc m)] ;; you could filter here if you want fns only
      (select-keys m [:name :doc]))))

(ns-docs 'clojure.repl)
=>
({:name apropos,
  :doc "Given a regular expression or stringable thing, return a seq of all
        public definitions in all currently-loaded namespaces that match the
        str-or-pattern."}
 ...
)

Then you can print those maps/strings if you want.

Upvotes: 1

Alan Thompson
Alan Thompson

Reputation: 29984

I would, instead, start here:

Note that doc is in the namespace clojure.repl, which reflects its intended usage (by a human in a repl). Here is some code that will also iterate on a namespace & print doc strings (using a different technique):

  (doseq [[fn-symbol fn-var] (ns-interns 'demo.core)]
    (newline)
    (println fn-symbol)
    (println (:doc (meta fn-var))))

where demo.core is the namespace of interest.

Note that ns-interns gives you both a symbol and var like:

fn-symbol  => <#clojure.lang.Symbol -main>
fn-var     => <#clojure.lang.Var #'demo.core/-main>

The meta function has lots of other info you may want to use someday:

(meta fn-var)   =>   
<#clojure.lang.PersistentArrayMap 
  { :arglists ([& args]), 
    :doc "The Main Man!", 
    :line 9, :column 1, 
    :file "demo/core.clj", 
    :name -main, 
    :ns #object[clojure.lang.Namespace 0x14c35a06 "demo.core"]}>

Upvotes: 2

Related Questions