Search This Blog

Monday, March 1, 2010

Conditioning the REPL

Clojure's REPL is fairly minimal on startup.

I find that I always want to load clojure.contrib.repl-utils, amongst other libraries, and I have some handy debugging and exploring functions which I like to have loaded when interacting with my programs.

Also, the otherwise excellent 'apropos' function find-doc only looks in namespaces which have been loaded, so it's no good for finding things when you don't know which namespace they're in.

For this reason, it's useful to require every namespace that you can find.

The number of batteries included in clojure is getting quite phenomenal, and find-doc, doc, and source and javadoc are good tools for exploring.

I also often want to know what the classpath and current directory are, and I sometimes find it frustrating to have to remember whether various functions take a string, a symbol or a quoted symbol.

Recently I've found myself putting all these little tweaks in a file which I can load first thing on starting a REPL. On my netbook requiring everything takes about 30 seconds.

 
;; LOAD ME WITH
;; (load-file "/home/john/hobby-code/require-all-snippet.clj")

;; This file conditions a repl in various ways that I do all the time.

;; Firstly we want to 'require' all the namespaces on the classpath
;; This ensures that find-doc and the like will work
(require 'clojure.contrib.find-namespaces)

;; Some namespaces may fail to load, so catch any exceptions thrown
(defn- require-may-fail [ns]
  (try

   (print "Attempting to require " ns ": ")
   (require ns)
   (println "success")
   (catch Exception e (println "couldn't require " ns "\nException\n" e "\n\n"))))


;; Generally we'd want clojure.*, clojure.contrib.*, and any project-specific namespaces
(defn require-all-namespaces-starting-with [strng]
  (doall (map require-may-fail 
              (filter #(. (str %) startsWith strng) 
                      (clojure.contrib.find-namespaces/find-namespaces-on-classpath)))))


;; The functions in these namespaces are so useful at the REPL that I want them 'use'd.
;; I.e. I want to be able to type 'source' rather than 'clojure.contrib.repl-utils/source'
(use 'clojure.contrib.repl-utils)
(use 'clojure.inspector)
(use 'clojure.contrib.pprint)
(use 'clojure.contrib.repl-utils)
(use 'clojure.contrib.trace)

;; It drives me up the wall that it's (doc re-pattern) but (find-doc "re-pattern").
;; Can use macros so that (fd re-pattern) (fd "re-pattern") and (fd 're-pattern) all mean the same thing
(defn- stringify [x]
  (println "stringify given" (str x))
  (let [s  (cond (string? x) x
                 (symbol? x) (str x)
                 (and (list? x) (= (first x) 'quote)) (str (second x))
                 :else (str x)) ]
    (println (str "translating to: \"" s "\""))
    s))



;; Sometimes I like to ask which public functions a namespace provides.
(defn- ns-publics-list [ns] (#(list (ns-name %) (map first (ns-publics %))) ns))
;; And occasionally which functions it pulls in (with refer or use)
(defn- ns-refers-list  [ns] (#(list (ns-name %) (map first (ns-refers %))) ns))


;; Nice pretty-printed versions of these functions, accepting strings, symbols or quoted symbol
(defmacro list-publics     
  ([]   `(pprint (ns-publics-list *ns*)))
  ([symbol-or-string] `(pprint (ns-publics-list (find-ns (symbol (stringify '~symbol-or-string)))))))

(defmacro list-refers
  ([]   `(pprint (ns-refers-list *ns*)))
  ([symbol-or-string] `(pprint (ns-refers-list (find-ns (symbol (stringify '~symbol-or-string)))))))

;; List all the namespaces
(defn list-all-ns [] (pprint (map ns-name (all-ns))))

;; List all public functions in all namespaces!
(defn list-publics-all-ns [] (pprint (map #(list (ns-name %) (map first (ns-publics %))) (all-ns))))

;; With all the namespaces loaded, find-doc can be overwhelming.
;; This is like find-doc, but just gives the associated names.

(defn- find-doc-names
  "Prints the name of any var whose documentation or name contains a match for re-string-or-pattern"
  [re-string-or-pattern]
    (let [re  (re-pattern re-string-or-pattern)]
      (doseq [ns (all-ns)
              v (sort-by (comp :name meta) (vals (ns-interns ns)))
              :when (and (:doc ^v)
                         (or (re-find (re-matcher re (:doc ^v)))
                             (re-find (re-matcher re (str (:name ^v))))))]
               (print v "\n"))))





;;find symbol or string in docs 
(defmacro fd [symbol-or-string] `(find-doc (stringify '~symbol-or-string)))

(defmacro fdn [symbol-or-string] `(find-doc-names (stringify '~symbol-or-string)))



;;debugging macro                                try: (* 2 (dbg (* 3 4)))
(defmacro dbg [x] `(let [x# ~x] (do (println '~x "->" x#) x#))) 

;;and pretty-printing version 
(defmacro ppdbg [x]`(let [x# ~x] (do (println "--")(pprint '~x)(println "->")(pprint x#) (println "--") x#))) 


;;and one for running tests 
(defmacro run-test [fn] `(test (resolve '~fn)))


;; Sometimes it's nice to check the classpath
(defn- get-classpath []
   (sort (map (memfn getPath) 
              (seq (.getURLs (java.lang.ClassLoader/getSystemClassLoader))))))

(defn print-classpath []
  (clojure.contrib.pprint/pprint (get-classpath)))

(defn get-current-directory []
  (. (java.io.File. ".") getCanonicalPath))


;;require everything from clojure and clojure.contrib, so that find-doc can find it
(require-all-namespaces-starting-with "clojure")

;;print the classpath
(println "Classpath:")
(print-classpath)

(println "Current Directory" (get-current-directory))

;;print the public functions in the current namespace
(println "Current Namespace")
(list-publics)


;;hint on how to require project specific namespaces
(println "to require all namespaces starting with example:")
(println "(require-all-namespaces-starting-with \"example\")")

7 comments:

  1. There's such a thing as user.clj that allows you to run code at REPL start up. I'm not using it myself, and I don't think it's documented very well, but IIRC a user.clj file gets loaded automatically when you start a REPL if one is found in your classpath. Or something like that... I have no idea how it mixes with slime. Do write a post if you can make it work :-)

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. It is possible to use '~/.lein/init.clj',
      ref. https://github.com/technomancy/leiningen/wiki/Upgrading#user-level-settings.
      This works fine for Clojure on the JVM.

      If you use Leiningen to start Clojure CLR on Windows (ref. https://github.com/kumarshantanu/lein-clr),
      you cannot use Clojure CLR interop functions, as init.clj is interpreted bei Leiningen on the JVM.
      However, pure Clojure code works also in this case.

      Delete
  2. Please put this on github or the equivalent. Thanks.

    ReplyDelete
  3. Quite nice and useful. Thanks for sharing...

    Found an issue in find-doc-names. Fails to compile for me with clojure.lang.LispReader$ReaderException: java.lang.Exception: Unmatched delimiter: ). Quickly scanned for errors - but appears to be ok. Any ideas?

    ReplyDelete
  4. Thanks for your script, I found myself doing your same logical steps in the REPL:

    I did some minor changes in order to work it with Clojure 1.2. Removed some "use", explicited ^v with (meta v

    @vitalyper: this resolve your issue :)

    Here it is:
    http://pastebin.com/dVivdqpB

    ReplyDelete
  5. Here an update for clojure 1.3. In order to use it you must add to the classpath project (on lein, project.clj):
    - [org.clojure/tools.trace "0.7.3"] -> tools.trace-0.7.3.jar
    - [org.clojure/tools.namespace "0.1.0"] -> tools.namespace-0.1.0.jar

    http://pastebin.com/cXL1wNVC

    ReplyDelete

Followers