Zen and the art of...

2009-12-21

Writing a Help Macro.

Emacs may seems like a pretty barebone tool for the uninitiated, but pack a lot of things under the hood. Especially when you're coding with a Lisp, Emacs really shines thanks to its renowned slime mode. Yet, most of the power of using a Lisp-like language with any editors lies in the use of the REPL. Clojure has a great set of functions and macros available to help you code using it, but these are dispersed around many libraries contained in clojure-contrib. As there's lots of such helpers, you have to remember a bunch of names and refer to documentation often before getting used to all of them. Having these functionalities packed into a single command could be very useful for beginners as well as for practitioners. We'll first pass trough the code and review it, then we'll show how to use the help macro.

Help Macro

We'll proceed in a top-bottom manner, starting with the namespace definition. We'll need some Java classes from clojure.lang package and also add some aliases for the clojure-contrib libraries we'll be using.

(ns help-macro
  "The help macro regroups clojure-contrib most useful help functions and
  macros into a single call."
  (import [clojure.lang IFn PersistentList Symbol])
  (require
    [clojure.contrib.classpath  :as classpath]
    [clojure.contrib.repl-utils :as repl-utils]
    [clojure.contrib.str-utils  :as str-utils]
    [clojure.contrib.ns-utils   :as ns-utils]))

Then comes the definition of *help-usage*, a simple var containing a string explaining how to use the help macro. That could have been included in the docstring, but I chose to emulate the way command line scripts work instead.

(def *help-usage*
  #^{:doc "Help macro usage text."}
  (str
    "Usage: (help pwd)\n"
    "       (help classpath)\n"
    "       (help dir <ns>)\n"
    "       (help docs <ns>)\n"
    "       (help vars <ns>)\n"
    "       (help source <symbol>)\n"
    "       (help <class> [<n>])\n"
    "       (help <expr>)\n"
    "       (help <string>)\n"
    "       (help ? <query-type>)"))

As you see, we'll support nine kinds of help queries, six of which use a command symbol while the three others are dispatched on the class of their first argument. Lets review each types of queries:

  • pwd - Returns the current working directory.
  • classpath - Prints the current classpath.
  • dir - Prints a sorted directory of public vars in a namespace.
  • docs - Prints documentation for the public vars in a namespace.
  • vars - Returns a sorted seq of symbols naming public vars in a namespace.
  • source - Returns a string of the source code for the given symbol, if it can find it.
  • <class> - Get help on class members, like clojure.contrib.repl-utils/show.
  • <expr> - Get help on the result of an expression, like clojure.contrib.repl-utils/expression-info.
  • <string> - Like find-doc, but easier to type.

The help usage message is nice, but it would be great to have more specific ones for each types of queries, like the above list. To do this, we'll create a help-usage function that will print various messages depending on the symbol given as first argument.

(defn help-usage
  "Returns documentation on help macro usage."
  [query-type & args]
  (condp = query-type
    'class     (doc clojure.contrib.repl-utils/show)
    'expr      (doc clojure.contrib.repl-utils/expression-info)
    'string    (doc find-doc)
    (println
      (condp = query-type
        'pwd       "Returns the current working directory."
        'classpath "Prints the current classpath."
        'dir       "Prints a sorted directory of public vars in a namespace."
        'docs      "Prints documentation for the public vars in a namespace."
        'vars      "Returns a sorted seq of symbols naming public vars in a namespace."
        'source    "Returns a string of the source code for the given symbol, if it can find it."
        "This type of help query is not recognized."))))

Each symbols are separated in two categories. For the simpler queries, we only display a description of what they do. For more complex ones though, we show the documentation for the original function or macro using the doc macro. The function accept other arguments only to prevent exceptions in case of unintentional input.

We'll now look at the generic-help multimethod, which will dispatch calls on the class of its first argument. We'll only respond to three classes: Class, Clojure's PersistentList and String.

(defmulti generic-help
  "Makes the help macro generic on its first argument if no command found."
  {:arglists '([query args])}
  (fn [query _] (class query)))

(defmethod generic-help Class
  [query args]
  (apply repl-utils/show (cons query args)))

(defmethod generic-help PersistentList
  [query args]
  (repl-utils/expression-info (second query)))

(defmethod generic-help String
  [query args]
  (find-doc query))

A thing to note here is the reason why we take the second argument of the list in the PersistentList method. This is so because the help macro quote all arguments it receives, but the expression query must be already quoted.

We'll also add a default dispatch method which displays a warning message followed by the help macro usage.

(defmethod generic-help :default
  [query _]
  (println "No help available for object of type " (class query))
  (help-usage))

All simple commands are contained in the *help-command* map, which is used by the help* function following it.

(def *help-commands*
  #^{:doc "This is a map containing all the commands for the help macro."}
  { 'pwd       #(.getCanonicalPath (java.io.File. "."))
    'classpath #(println (str-utils/str-join "\n" (classpath/classpath)))
    'dir       ns-utils/print-dir
    'docs      ns-utils/print-docs
    'vars      ns-utils/ns-vars
    'source    (comp println repl-utils/get-source)})

(defn help*
  "Driver for the help macro."
  [query args]
  (if-let [sc (get *help-commands* query)]
    (apply sc args)
    (generic-help (if (symbol? query)
                    (resolve query)
                    query) args)))

This function look into the *help-command* map to see if it contains the given query symbol. If found, it calls the associated runnable, else it calls generic-help. Before calling the multimethod, we first try to resolve the query quoted expression in case it's a symbol. This is because this function receives only the symbol of a class when help is called with a class name.

Finally, here's the help macro in all it's glory.

(defmacro help
  "Get help for various kind of expressions, use without arguments for
  detailed usage."
  ([] `(println *help-usage*))
  ([query & args]
    (let [quoted-args (map #(list 'quote %) args)]
      (if (= '? query)
        `(help-usage ~@quoted-args)
        `(help* '~query (list ~@quoted-args))))))

Usage

Here's some examples of using the help macro:

user> (help pwd)
"C:\\"
user> (help classpath)
g:\libraries\java\swank-clojure-1.0-SNAPSHOT.jar
g:\libraries\java\servlet-api-2.5-20081211.jar
...
nil
user> (help dir help-macro)
*help-commands*
*help-usage*
generic-help
help
help*
help-usage
nil
user> (help docs help-macro)
-------------------------
help-macro/*help-commands*
nil
  nil
...
nil
user> (help vars help-macro)
(*help-commands* *help-usage* generic-help help help* help-usage)
user> (help source +)
(defn +
  "Returns the sum of nums. (+) returns 0."
  {:inline (fn [x y] `(. clojure.lang.Numbers (add ~x ~y)))
   :inline-arities #{2}}
  ([] 0)
  ([x] (cast Number x))
  ([x y] (. clojure.lang.Numbers (add x y)))
  ([x y & more]
   (reduce + (+ x y) more)))
nil
user> (help String)
===  public final java.lang.String  ===
[ 0] static CASE_INSENSITIVE_ORDER : Comparator
[ 1] static copyValueOf : String (char[])
...
nil
user> (help String 1)
#<Method public static java.lang.String java.lang.String.copyValueOf(char[])>
user> (help '1)
{:class java.lang.Integer, :primitive? false}
user> (help '(int 1))
{:class int, :primitive? true}
user> (help "help")
-------------------------
clojure.main/help-opt
([_ _])
  Print help text for main
-------------------------
clojure.main/main
([& args])
...

Installing

You can find the complete code here. To use it, place that file in your classpath and create an user script to be called when stating a REPL. From there, you simply have to add the help-macro namespace with the use function if you want to be able to just type "(help...". To have it around in every namespace, you could add the following function in the same file than the macro and use it instead of in-ns.

(defn to-ns [n]
  (in-ns n)
  (use 'help-macro))

Any comments, suggestions, improvements, insults?

2 comments:

  1. I've gone ahead and tossed this into version control in my Github account to make it a little easier to work with: http://github.com/mwilliams/help_macro

    Great write-up! Feel free to toss in your own repo if you would like to make this any sort of official project since you're the original author. If you do so, ping me and I'll go ahead and wipe my repo and just fork yours (or follow it).

    Thanks!

    ReplyDelete
  2. Great! I didn't put it on Github just because this macro is part of a bigger set of REPL helpers that I intend to put there once it is all cleaned up.

    ReplyDelete

About Me

My photo
Quebec, Canada
Your humble servant.