Creating a Ruby-like `binding.pry` macro in Clojure

Viewed 93

I've been struggling to create a macro that'll allow me to dynamically bind whatever is in the &env into a binding form and then delegate to a pry like function to open a REPL that can see those bound &env symbols.

My simplistic pry func, which works as expected

(defn pry []
  (print (str "pry(" *ns* ")> "))
  (flush)
  (let [expr (read)]
    (when-not (= expr :exit)
      (println (eval expr))
      (recur))))

Using the pry func:

clojure-noob.core=> (def a 1)
#'clojure-noob.core/a
clojure-noob.core=> (pry)
pry(clojure-noob.core)> (+ a 1)
2
pry(clojure-noob.core)> :exit
nil
clojure-noob.core=>

My attempt at creating a dynamic invocation of binding:

(defmacro binding-pry []
  (let [ks (keys &env)]
    `(let [ks# '~ks
           vs# [~@ks]
           bs# (vec (interleave ks# vs#))]
       (binding bs# (pry)))))

However, this fails because the inner symbol bs# is not expanded to an actual vector but instead is the generated symbol and binding tosses a clojure.core/binding requires a vector for its binding exception.

clojure-noob.core=> (let [a 1 b 2] (binding-pry))
Syntax error macroexpanding clojure.core/binding at (/tmp/form-init14332359378145135257.clj:1:16).
clojure.core/binding requires a vector for its binding in clojure-noob.core:

clojure-noob.core=> 

The code quoted form with a debug print, the bs# symbol is resolved when printing but I don't know how to make it resolve to a vector when constructing the binding form.

(defmacro binding-pry []
  (let [ks (keys &env)]
    `(let [ks# '~ks
           vs# [~@ks]
           bs# (vec (interleave ks# vs#))]
       (println bs#)
       `(binding bs# (pry)))))

clojure-noob.core=> (let [a 1 b 2] (binding-pry))
[a 1 b 2]
(clojure.core/binding clojure-noob.core/bs__2464__auto__ (clojure-noob.core/pry))
clojure-noob.core=>

I'm very confident I'm tackling this incorrectly but I don't see another approach.

2 Answers

A first step towards improving your attempt could be writing:

(defmacro binding-pry []
  (let [ks (keys &env)]
    `(binding [~@(interleave ks ks)] (pry))))

This still doesn't work because binding expects that the symbols can be resolved to existing dynamic vars. To tackle this problem you could make binding-pry introduce such vars as shown below:

(defmacro binding-pry []
  (let [ks (keys &env)]
    `(do
       ~@(map (fn [k] `(def ~(with-meta k {:dynamic true}))) ks)
       (binding [~@(interleave ks ks)] (pry)))))

But this can have undesirable side-effects, like polluting the namespace with new var-names or making existing vars dynamic. So I would prefer an approach like the one mentioned in amalloy's answer but with a better implementation of eval-in-context (see my comment there).

To write a self-contained answer based on your pry function, let's first define eval-in which evaluates a form in an environment:

(defn eval-in [env form]
  (apply
    (eval `(fn* [~@(keys env)] ~form))
    (vals env)))

Then let's modify pry to take an environment as an argument and use eval-in instead of eval:

(defn pry [env]
  (let [prompt (str "pry(" *ns* ")> ")]
    (loop []
      (print prompt)
      (flush)
      (let [expr (read)]
        (when-not (= expr :exit)
          (println (eval-in env expr))
          (recur))))))

An equivalent, less primitive version could be:

(defn pry [env]
  (->> (repeatedly (let [prompt (str "pry(" *ns* ")> ")]
                     #(do (print prompt) (flush) (read))))
       (take-while (partial not= :exit))
       (run! (comp println (partial eval-in env)))))

Now we can define binding-pry as follows:

(defmacro binding-pry []
  `(pry ~(into {}
           (map (juxt (partial list 'quote) identity))
           (keys &env))))

Finally, here is a direct/"spaghetti" implementation of binding-pry:

(defmacro binding-pry []
  (let [ks (keys &env)]
    `(->> (repeatedly (let* [prompt# (str "pry(" *ns* ")> ")]
                        #(do (print prompt#) (flush) (read))))
          (take-while (partial not= :exit))
          (run! (comp println
                      #((eval `(fn* [~~@(map (partial list 'quote) ks)] ~%))
                        ~@ks))))))
Related